diff --git a/.github/workflows/docc.yml b/.github/workflows/docc.yml
new file mode 100644
index 0000000..0b7c4d2
--- /dev/null
+++ b/.github/workflows/docc.yml
@@ -0,0 +1,54 @@
+# Generates and Deploys DocC documentation to Github pages
+# See: https://maxxfrazer.medium.com/deploying-docc-with-github-actions-218c5ca6cad5
+name: DocC
+
+on:
+ push:
+ branches: [ "main" ]
+
+# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+# Allow one concurrent deployment
+concurrency:
+ group: "pages"
+ cancel-in-progress: true
+
+jobs:
+
+ # Single deploy job since we're just deploying
+ docc:
+
+ env:
+ XCODE_VERSION: 'Xcode_16.4'
+
+ environment:
+ # Must be set to this for deploying to GitHub Pages
+ name: github-pages
+ url: ${{steps.deployment.outputs.page_url}}
+
+ runs-on: macos-15
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Select Xcode
+ run: sudo xcode-select -s "/Applications/$XCODE_VERSION.app"
+ - name: Generate DocC
+ run: xcodebuild docbuild -scheme OAuthKit -derivedDataPath /tmp/docbuild -destination 'generic/platform=macOS';
+ - name: Transform DocC
+ run: |
+ $(xcrun --find docc) process-archive \
+ transform-for-static-hosting /tmp/docbuild/Build/Products/Debug/OAuthKit.doccarchive \
+ --hosting-base-path OAuthKit \
+ --output-path docs;
+ - name: Update Index
+ run: echo "" > docs/index.html;
+ - name: Upload Pages Artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: 'docs'
+ - name: Deploy to GitHub Pages
+ uses: actions/deploy-pages@v4
diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml
index ca3884f..51c42f2 100644
--- a/.github/workflows/swift.yml
+++ b/.github/workflows/swift.yml
@@ -12,6 +12,7 @@ on:
jobs:
build:
+
env:
XCODE_VERSION: 'Xcode_16.4'
runs-on: macos-15
diff --git a/.gitignore b/.gitignore
index 8300ca8..f3a5856 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
+Package.resolved
\ No newline at end of file
diff --git a/.spi.yml b/.spi.yml
new file mode 100644
index 0000000..85d47a3
--- /dev/null
+++ b/.spi.yml
@@ -0,0 +1,3 @@
+version: 1
+external_links:
+ documentation: "https://codefiesta.github.io/OAuthKit/documentation/oauthkit"
diff --git a/Sources/OAuthKit/Network/NetworkMonitor.swift b/Sources/OAuthKit/Network/NetworkMonitor.swift
index 37b12b7..d3ae851 100644
--- a/Sources/OAuthKit/Network/NetworkMonitor.swift
+++ b/Sources/OAuthKit/Network/NetworkMonitor.swift
@@ -8,7 +8,7 @@
import Network
import Observation
-/// A type that broadcasts network reachability via Combine event publishing.
+/// An `Observable` type that publishes network reachability information.
@MainActor
@Observable
public final class NetworkMonitor {
diff --git a/Sources/OAuthKit/OAuth+DeviceCode.swift b/Sources/OAuthKit/OAuth+DeviceCode.swift
index 942c43c..8a6f3a9 100644
--- a/Sources/OAuthKit/OAuth+DeviceCode.swift
+++ b/Sources/OAuthKit/OAuth+DeviceCode.swift
@@ -10,8 +10,8 @@ import Foundation
extension OAuth {
/// A codable type that holds device code information.
- /// See: https://auth0.com/docs/get-started/authentication-and-authorization-flow/device-authorization-flow
- /// See: https://www.oauth.com/playground/device-code.html
+ /// - SeeAlso:
+ /// [Requesting a Device Code](https://www.oauth.com/playground/device-code.html)
public struct DeviceCode: Codable, Equatable, Sendable {
/// A constant for the oauth grant type.
diff --git a/Sources/OAuthKit/OAuth+Provider.swift b/Sources/OAuthKit/OAuth+Provider.swift
index 237e45c..6e83e4f 100644
--- a/Sources/OAuthKit/OAuth+Provider.swift
+++ b/Sources/OAuthKit/OAuth+Provider.swift
@@ -57,7 +57,7 @@ extension OAuth {
case debug
}
- /// Public initializer
+ /// Public initializer.
/// - Parameters:
/// - id: The provider unique id
/// - icon: The provider icon
diff --git a/Sources/OAuthKit/OAuth+State.swift b/Sources/OAuthKit/OAuth+State.swift
index e7127b2..87031c0 100644
--- a/Sources/OAuthKit/OAuth+State.swift
+++ b/Sources/OAuthKit/OAuth+State.swift
@@ -9,7 +9,44 @@ import Foundation
extension OAuth {
- /// Holds the OAuth state that is published to subscribers via the `OAuth.state` property publisher.
+ /// Holds the OAuth state that is published to subscribers via the ``state`` property.
+ ///
+ /// A n example of observing the ``state`` property in SwiftUI:
+ /// ```swift
+ ///struct ContentView: View {
+ ///
+ /// @Environment(\.oauth)
+ /// var oauth: OAuth
+ ///
+ /// var body: some View {
+ /// VStack {
+ /// switch oauth.state {
+ /// case .empty:
+ /// providerList
+ /// case .authorizing(let provider, let grantType):
+ /// Text("Authorizing [\(provider.id)] with [\(grantType.rawValue)]")
+ /// case .requestingAccessToken(let provider):
+ /// Text("Requesting Access Token [\(provider.id)]")
+ /// case .requestingDeviceCode(let provider):
+ /// Text("Requesting Device Code [\(provider.id)]")
+ /// case .authorized(let provider, _):
+ /// Button("Authorized [\(provider.id)]") {
+ /// oauth.clear()
+ /// }
+ /// case .receivedDeviceCode(_, let deviceCode):
+ /// Text("To login, visit")
+ /// Text(.init("[\(deviceCode.verificationUri)](\(deviceCode.verificationUri))"))
+ /// .foregroundStyle(.blue)
+ /// Text("and enter the following code:")
+ /// Text(deviceCode.userCode)
+ /// }
+ /// }
+ /// .onChange(of: oauth.state) { oldState, newState in
+ /// handle(state: newState)
+ /// }
+ /// }
+ /// }
+ /// ```
public enum State: Equatable, Sendable {
/// The state is empty and no authorizations or tokens have been issued.
diff --git a/Sources/OAuthKit/OAuth+Token.swift b/Sources/OAuthKit/OAuth+Token.swift
index 5094df8..7267d77 100644
--- a/Sources/OAuthKit/OAuth+Token.swift
+++ b/Sources/OAuthKit/OAuth+Token.swift
@@ -10,8 +10,8 @@ import Foundation
extension OAuth {
/// A codable type that holds oauth token information.
- /// See: https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/
- /// See: https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
+ /// - SeeAlso:
+ /// [Access Token Response](https://www.oauth.com/oauth2-servers/access-tokens/access-token-response/)
public struct Token: Codable, Equatable, Sendable {
/// The access token string as issued by the authorization server.
diff --git a/Sources/OAuthKit/OAuth.swift b/Sources/OAuthKit/OAuth.swift
index fb6b12f..534c5b0 100644
--- a/Sources/OAuthKit/OAuth.swift
+++ b/Sources/OAuthKit/OAuth.swift
@@ -29,8 +29,30 @@ public enum OAError: Error {
case keychain
}
-/// Provides an observable OAuth 2.0 implementation.
-/// See: https://datatracker.ietf.org/doc/html/rfc6749
+/// Provides an `Observable` OAuth 2.0 implementation that emits ``OAuth/state`` changes when
+/// an authorization flow is started by calling ``OAuth/authorize(provider:grantType:)``.
+///
+/// You can create an observable OAuth object using the ``OAuth/init(_:options:)`` or ``OAuth/init(providers:options:)`` initializers, or
+/// you can access a default OAuth object via SwiftUI ``SwiftUICore/EnvironmentValues`` via the following:
+///
+/// ```swift
+/// @Environment(\.oauth)
+/// var oauth: OAuth
+/// ```
+///
+/// An OAuth object can also be highly customized when passed a dictionary of ``Option`` values into it's iniitializers.
+/// ```swift
+/// let options: [OAuth.Option: Any] = [
+/// .applicationTag: "com.bundle.Idenfitier",
+/// .autoRefresh: true,
+/// .requireAuthenticationWithBiometricsOrCompanion: true,
+/// .useNonPersistentWebDataStore: true
+/// ]
+/// let oauth: OAuth = .init(.module, options: options)
+/// ```
+///
+/// - SeeAlso:
+/// [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)
@MainActor
@Observable
public final class OAuth {
diff --git a/Sources/OAuthKit/OAuthKit.docc/Extensions/OAuth.md b/Sources/OAuthKit/OAuthKit.docc/Extensions/OAuth.md
new file mode 100644
index 0000000..44206c7
--- /dev/null
+++ b/Sources/OAuthKit/OAuthKit.docc/Extensions/OAuth.md
@@ -0,0 +1,17 @@
+# ``OAuthKit/OAuth``
+
+## Topics
+
+### Creating an Observable OAuth
+
+- ``init(_:options:)``
+- ``init(providers:options:)``
+
+### Starting an authorization flow
+
+- ``authorize(provider:grantType:)``
+
+### Observing an OAuth object
+
+- ``state``
+- ``providers``
diff --git a/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md b/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md
new file mode 100644
index 0000000..f20f516
--- /dev/null
+++ b/Sources/OAuthKit/OAuthKit.docc/GettingStarted.md
@@ -0,0 +1,122 @@
+# Getting Started with OAuthKit
+
+Create an ``OAuth`` instance and start an authorization flow.
+
+@Metadata {
+ @PageKind(article)
+}
+
+## Overview
+
+OAuth 2.0 authorization flows are started by calling ``OAuth/authorize(provider:grantType:)`` with an ``OAuth/Provider`` and ``OAuth/GrantType``.
+
+```swift
+// Create an observable OAuth object
+let oauth: OAuth = .init(.main)
+
+// Start an authorization flow
+let grantType: OAuth.GrantType = .pkce(.init())
+oauth.authorize(provider: provider, grantType: grantType)
+```
+
+## Observing OAuth State
+
+Once an OAuth 2.0 authorization flow has been started in an application, observers of an OAuth object will be notified of next step events in that flow and can react accordingly. For example:
+
+```swift
+@main
+struct OAuthApp: App {
+
+ @Environment(\.oauth)
+ var oauth: OAuth
+
+ /// Build the scene body
+ var body: some Scene {
+
+ WindowGroup {
+ ContentView()
+ }
+
+ #if canImport(WebKit)
+ WindowGroup(id: "oauth") {
+ OAWebView(oauth: oauth)
+ }
+ #endif
+ }
+}
+
+struct ContentView: View {
+
+ @Environment(\.oauth)
+ var oauth: OAuth
+
+ @Environment(\.openWindow)
+ var openWindow
+
+ @Environment(\.dismissWindow)
+ private var dismissWindow
+
+ var body: some View {
+ VStack {
+ switch oauth.state {
+ case .empty:
+ providerList
+ case .authorizing(let provider, let grantType):
+ Text("Authorizing [\(provider.id)] with [\(grantType.rawValue)]")
+ case .requestingAccessToken(let provider):
+ Text("Requesting Access Token [\(provider.id)]")
+ case .requestingDeviceCode(let provider):
+ Text("Requesting Device Code [\(provider.id)]")
+ case .authorized(let provider, _):
+ Button("Authorized [\(provider.id)]") {
+ oauth.clear()
+ }
+ case .receivedDeviceCode(_, let deviceCode):
+ Text("To login, visit")
+ Text(.init("[\(deviceCode.verificationUri)](\(deviceCode.verificationUri))"))
+ .foregroundStyle(.blue)
+ Text("and enter the following code:")
+ Text(deviceCode.userCode)
+ .padding()
+ .border(Color.primary)
+ .font(.title)
+ }
+ }
+ .onChange(of: oauth.state) { _, state in
+ handle(state: state)
+ }
+ }
+
+ /// Displays a list of oauth providers.
+ var providerList: some View {
+ List(oauth.providers) { provider in
+ Button(provider.id) {
+ authorize(provider: provider)
+ }
+ }
+ }
+
+ /// Starts the authorization process for the specified provider.
+ /// - Parameter provider: the provider to begin authorization for
+ private func authorize(provider: OAuth.Provider) {
+ let grantType: OAuth.GrantType = .pkce(.init())
+ oauth.authorize(provider: provider, grantType: grantType)
+ }
+
+ /// Reacts to oauth state changes by opening or closing authorization windows.
+ /// - Parameter state: the published state change
+ private func handle(state: OAuth.State) {
+ #if canImport(WebKit)
+ switch state {
+ case .empty, .requestingAccessToken, .requestingDeviceCode:
+ break
+ case .authorizing, .receivedDeviceCode:
+ openWindow(id: "oauth")
+ case .authorized(_, _):
+ dismissWindow(id: "oauth")
+ }
+ #endif
+ }
+}
+```
+
diff --git a/Sources/OAuthKit/OAuthKit.docc/OAuthKit.md b/Sources/OAuthKit/OAuthKit.docc/OAuthKit.md
new file mode 100644
index 0000000..89ebac4
--- /dev/null
+++ b/Sources/OAuthKit/OAuthKit.docc/OAuthKit.md
@@ -0,0 +1,31 @@
+# ``OAuthKit``
+
+A modern and observable framework for implementing OAuth 2.0 authorization flows.
+
+@Metadata {
+ @PageColor(purple)
+}
+
+## Overview
+
+OAuthKit offers a robust, type-safe, and performant OAuth 2.0 implementation using the observer design pattern. This pattern allows applications to observe an ``OAuth`` object and be notified of ``OAuth/State`` changes.
+
+This has the advantage of avoiding direct coupling with an authorization flow, enabling highly flexible and fine-grained control when interacting with an ``OAuth/Provider``. It also allows updates across multiple observers.
+
+### Featured
+
+@Links(visualStyle: detailedGrid) {
+ -
+ -
+}
+
+## Topics
+
+### Essentials
+
+-
+-
+- ``OAuth``
+
+### State Observability
+- ``OAuth/state``
diff --git a/Sources/OAuthKit/OAuthKit.docc/SampleCode.md b/Sources/OAuthKit/OAuthKit.docc/SampleCode.md
new file mode 100644
index 0000000..030ecbd
--- /dev/null
+++ b/Sources/OAuthKit/OAuthKit.docc/SampleCode.md
@@ -0,0 +1,15 @@
+# OAuthKit Sample: Integrating OAuthKit into an App
+
+Integrate OAuthKit into a multiplatform app with support for iOS, macOS, tvOS, visionOS, and watchOS.
+
+@Metadata {
+ @CallToAction(
+ purpose: link,
+ url: "https://github.com/codefiesta/OAuthSample"
+ )
+ @PageKind(sampleCode)
+}
+
+## Overview
+
+This sample creates _OAuthSample_, an app for demonstrating how to get up and running quickly with OAuthKit.
diff --git a/Sources/OAuthKit/Views/OAWebView.swift b/Sources/OAuthKit/Views/OAWebView.swift
index 57bfcd4..7e9069b 100644
--- a/Sources/OAuthKit/Views/OAWebView.swift
+++ b/Sources/OAuthKit/Views/OAWebView.swift
@@ -10,7 +10,7 @@ import SwiftUI
import WebKit
/// A UIViewRepresentable / NSViewRepresentable wrapper type that coordinates
-/// oauth authorization flows inside a WKWebView.
+/// oauth authorization flows inside a `WKWebView`.
@MainActor
public struct OAWebView {
diff --git a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift
index cf1992e..8f23c82 100644
--- a/Sources/OAuthKit/Views/OAWebViewCoordinator.swift
+++ b/Sources/OAuthKit/Views/OAWebViewCoordinator.swift
@@ -9,6 +9,7 @@
import SwiftUI
import WebKit
+/// Coordinates ``OAuth/state-swift.property`` changes inside a ``OAWebView``.
@MainActor
public class OAWebViewCoordinator: NSObject {