From 5c723cb25b8908558a9e9b474837e822cc1f6b58 Mon Sep 17 00:00:00 2001 From: hstdt Date: Thu, 29 May 2025 10:52:39 +0800 Subject: [PATCH] Transfer to Observation --- README.md | 17 +++---- .../EnvironmentValues+Extensions.swift | 4 +- .../Extensions/URLRequest+Extensions.swift | 1 + Sources/OAuthKit/OAuth.swift | 47 ++++++++++--------- Sources/OAuthKit/Views/OAWebView.swift | 2 +- Tests/OAuthKitTests/OAuthTests.swift | 4 +- 6 files changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 8ea4bff..a8a99f3 100644 --- a/README.md +++ b/README.md @@ -21,19 +21,16 @@ import SwiftUI @main struct OAuthApp: App { - @Environment(\.oauth) - var oauth: OAuth - /// Build the scene body var body: some Scene { WindowGroup { ContentView() - }.environmentObject(oauth) + } WindowGroup(id: "oauth") { OAWebView() - }.environmentObject(oauth) + } } } @@ -45,7 +42,7 @@ struct ContentView: View { @Environment(\.dismissWindow) private var dismissWindow - @EnvironmentObject + @Environment(\.oauth) var oauth: OAuth var body: some View { @@ -63,8 +60,8 @@ struct ContentView: View { } } } - .onReceive(oauth.$state) { state in - handle(state: state) + .onChange(of: oauth.state) { + handle(state: oauth.state) } } @@ -96,7 +93,7 @@ struct ContentView: View { By default, the easiest way to configure OAuthKit is to simply drop an `oauth.json` file into your main bundle and it will get automatically loaded into your swift application and available as an [EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject). You can find an example `oauth.json` file [here](https://github.com/codefiesta/OAuthKit/blob/main/Tests/OAuthKitTests/Resources/oauth.json). ```swift - @EnvironmentObject + @Environment(\.oauth) var oauth: OAuth ``` @@ -111,7 +108,7 @@ If you are building your OAuth Providers programatically (recommended for produc ```swift let providers: [OAuth.Provider] = ... - let options: [OAuth.Option: Any] = [.applicationTag: "com.bundle.identifier"] + let options: [OAuth.Option: Sendable] = [.applicationTag: "com.bundle.identifier"] let oauth: OAuth = .init(providers: providers, options: options) ``` diff --git a/Sources/OAuthKit/Extensions/EnvironmentValues+Extensions.swift b/Sources/OAuthKit/Extensions/EnvironmentValues+Extensions.swift index 1b72329..9b18a7e 100644 --- a/Sources/OAuthKit/Extensions/EnvironmentValues+Extensions.swift +++ b/Sources/OAuthKit/Extensions/EnvironmentValues+Extensions.swift @@ -16,8 +16,8 @@ public extension EnvironmentValues { } } -struct OAuthKey: EnvironmentKey { +struct OAuthKey: @preconcurrency EnvironmentKey { /// The default OAuth instance that is loaded into the environment. - static let defaultValue: OAuth = .init(.main) + @MainActor static let defaultValue: OAuth = .init(.main) } diff --git a/Sources/OAuthKit/Extensions/URLRequest+Extensions.swift b/Sources/OAuthKit/Extensions/URLRequest+Extensions.swift index f4b5231..c88532c 100644 --- a/Sources/OAuthKit/Extensions/URLRequest+Extensions.swift +++ b/Sources/OAuthKit/Extensions/URLRequest+Extensions.swift @@ -14,6 +14,7 @@ public extension URLRequest { /// Attempts to set the authorization header using the access token. /// - Parameter oath: the oauth holder + @MainActor mutating func addAuthorization(oath: OAuth) { switch oath.state { case .authorized(let auth): diff --git a/Sources/OAuthKit/OAuth.swift b/Sources/OAuthKit/OAuth.swift index 99ffbc9..d60972c 100644 --- a/Sources/OAuthKit/OAuth.swift +++ b/Sources/OAuthKit/OAuth.swift @@ -7,6 +7,7 @@ import Combine import CryptoKit import Foundation +import Observation /// The default file name that holds the list of providers. private let defaultResourceName = "oauth" @@ -24,7 +25,9 @@ public enum OAError: Error { /// Provides an observable OAuth 2.0 implementation. /// See: https://datatracker.ietf.org/doc/html/rfc6749 -public class OAuth: NSObject, ObservableObject, @unchecked Sendable { +@MainActor +@Observable +public final class OAuth: NSObject { /// Keys and values used to specify loading or runtime options. public struct Option: Hashable, Equatable, RawRepresentable, Sendable { @@ -193,31 +196,35 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable { } /// A published list of available OAuth providers to choose from. - @Published public var providers = [Provider]() /// An observable published oauth state. - @Published public var state: State = .empty /// The url session to use for communicating with providers. + @ObservationIgnored private lazy var urlSession: URLSession = { let configuration = URLSessionConfiguration.default return URLSession(configuration: URLSessionConfiguration.default) }() + @ObservationIgnored private var tasks = [Task<(), any Error>]() - private var options: [Option: Any]? + @ObservationIgnored + private var options: [Option: Sendable]? + @ObservationIgnored private let networkMonitor = NetworkMonitor() + @ObservationIgnored private var keychain: Keychain = .default /// Combine subscribers. + @ObservationIgnored private var subscribers = Set() /// Initializes the OAuth service with the specified providers. /// - Parameters: /// - providers: the list of oauth providers - public init(providers: [Provider] = [Provider](), options: [Option: Any]? = nil) { + public init(providers: [Provider] = [Provider](), options: [Option: Sendable]? = nil) { super.init() self.options = options self.providers = providers @@ -230,7 +237,7 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable { /// - Parameters: /// - bundle: the bundle to load the oauth provider configuration information from. /// - options: the initialization options to apply - public init(_ bundle: Bundle, options: [Option: Any]? = nil) { + public init(_ bundle: Bundle, options: [Option: Sendable]? = nil) { super.init() self.options = options self.providers = loadProviders(bundle) @@ -268,8 +275,7 @@ public class OAuth: NSObject, ObservableObject, @unchecked Sendable { private func restore() async { for provider in providers { if let authorization: OAuth.Authorization = try? keychain.get(key: provider.id), !authorization.isExpired { - await publish(state: .authorized(authorization)) - break + publish(state: .authorized(authorization)) } } } @@ -300,9 +306,9 @@ public extension OAuth { @discardableResult func requestAccessToken(provider: Provider, code: String) async -> Result { // Publish the state - await publish(state: .requestingAccessToken(provider)) + publish(state: .requestingAccessToken(provider)) guard var urlComponents = URLComponents(string: provider.accessTokenURL.absoluteString) else { - await publish(state: .empty) + publish(state: .empty) return .failure(.malformedURL) } var queryItems = [URLQueryItem]() @@ -312,32 +318,32 @@ public extension OAuth { queryItems.append(URLQueryItem(name: "redirect_uri", value: provider.redirectURI)) urlComponents.queryItems = queryItems guard let url = urlComponents.url else { - await publish(state: .empty) + publish(state: .empty) return .failure(.malformedURL) } var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Accept") guard let (data, _) = try? await urlSession.data(for: request) else { - await publish(state: .empty) + publish(state: .empty) return .failure(.badResponse) } // Decode the token let decoder = JSONDecoder() guard let token = try? decoder.decode(Token.self, from: data) else { - await publish(state: .empty) + publish(state: .empty) return .failure(.decoding) } // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - await publish(state: .empty) + publish(state: .empty) return .failure(.keychain) } - await publish(state: .authorized(authorization)) + publish(state: .authorized(authorization)) return .success(token) } @@ -346,7 +352,7 @@ public extension OAuth { Task { debugPrint("⚠️ [Clearing oauth state]") keychain.clear() - await publish(state: .empty) + publish(state: .empty) } } @@ -370,27 +376,26 @@ public extension OAuth { request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Accept") guard let (data, _) = try? await urlSession.data(for: request) else { - return await publish(state: .empty) + return publish(state: .empty) } // Decode the token let decoder = JSONDecoder() guard let token = try? decoder.decode(Token.self, from: data) else { - return await publish(state: .empty) + return publish(state: .empty) } // Store the authorization let authorization = Authorization(issuer: provider.id, token: token) guard let stored = try? keychain.set(authorization, for: authorization.issuer), stored else { - return await publish(state: .empty) + return publish(state: .empty) } - await publish(state: .authorized(authorization)) + publish(state: .authorized(authorization)) } } /// Publishes state on the main thread. /// - Parameter state: the new state information to publish out on the main thread. - @MainActor private func publish(state: State) { switch state { case .authorized(let auth): diff --git a/Sources/OAuthKit/Views/OAWebView.swift b/Sources/OAuthKit/Views/OAWebView.swift index 35b87fd..37e6f81 100644 --- a/Sources/OAuthKit/Views/OAWebView.swift +++ b/Sources/OAuthKit/Views/OAWebView.swift @@ -14,7 +14,7 @@ import WebKit @MainActor public struct OAWebView { - @EnvironmentObject + @Environment(\.oauth) var oauth: OAuth let view = WKWebView() diff --git a/Tests/OAuthKitTests/OAuthTests.swift b/Tests/OAuthKitTests/OAuthTests.swift index af0d9d2..0ace4d3 100644 --- a/Tests/OAuthKitTests/OAuthTests.swift +++ b/Tests/OAuthKitTests/OAuthTests.swift @@ -14,8 +14,8 @@ final class OAuthTests { /// Tests the init method using a custom bundle. @Test("Initializing providers") func whenInitializing() async throws { - let oauth: OAuth = .init(.module) - let providers = oauth.providers + let oauth: OAuth = await .init(.module) + let providers = await oauth.providers #expect(providers.isNotEmpty) }