From 9702e65a78eafe6255d48dce47c320dad844ee27 Mon Sep 17 00:00:00 2001 From: Gil Shapira Date: Sun, 29 Dec 2024 14:17:57 +0200 Subject: [PATCH] Add handleURL function to replace flow resume (#79) --- README.md | 6 ++-- src/DescopeKit.swift | 52 +++++++++++++++++++++++++++++++++ src/flows/Flow.swift | 39 ------------------------- src/flows/FlowCoordinator.swift | 36 ++++++++++------------- src/sdk/SDK.swift | 15 ++++++++++ 5 files changed, 85 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 8580017..74ce9f1 100644 --- a/README.md +++ b/README.md @@ -162,13 +162,13 @@ guide to learn more. When your application delegate is notified about a universal link being triggered, you'll need to provide it to the flow so it can continue with the authentication. See the documentation -for `DescopeFlow.resume(with:)` for more details. +for `Descope.handleURL` for more details. ```swift func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } - DescopeFlow.current?.resume(with: url) - return true + let handled = Descope.handleURL(url) + return handled } ``` diff --git a/src/DescopeKit.swift b/src/DescopeKit.swift index f824bd4..6c5340a 100644 --- a/src/DescopeKit.swift +++ b/src/DescopeKit.swift @@ -1,4 +1,6 @@ +import Foundation + /// Provides functions for working with the Descope API. /// /// This singleton object is provided as a simple way to access the Descope SDK from anywhere @@ -93,3 +95,53 @@ public extension Descope { /// Provides functions for exchanging access keys for session tokens. static var accessKey: DescopeAccessKey { sdk.accessKey } } + +/// Support for working with Universal Links. +public extension Descope { + /// Resumes an ongoing authentication that's waiting for Magic Link authentication. + /// + /// When a flow performs authentication with Magic Link at some point it will wait for + /// the user to receive an email and tap on the authentication link provided inside. + /// The host application is expected to intercept this URL via Universal Links and + /// resume the running flow with it. + /// + /// You can do this by calling this function and passing the URL from the Universal Link. + /// For example, in a SwiftUI application: + /// + /// ```swift + /// @main + /// struct MyApp: App { + /// // ... + /// var body: some Scene { + /// WindowGroup { + /// ContentView().onOpenURL { url in + /// Descope.handleURL(url) + /// } + /// } + /// } + /// } + /// ``` + /// + /// You can pass the return value of this function directly to the `UIApplicationDelegate` + /// method for handling Universal Links. For example: + /// + /// ```swift + /// @main + /// class AppDelegate: UIResponder, UIApplicationDelegate { + /// // ... + /// func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + /// guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let url = userActivity.webpageURL else { return false } + /// return Descope.handleURL(url) + /// } + /// } + /// ``` + /// + /// - Parameter url: The `url` to use for resuming the authentication. + /// + /// - Returns: `true` when an ongoing authentication handled the URL or `false` to + /// let the caller know that the function didn't handle it. + @discardableResult @MainActor + static func handleURL(_ url: URL) -> Bool { + return sdk.handleURL(url) + } +} diff --git a/src/flows/Flow.swift b/src/flows/Flow.swift index 9101203..cc812cd 100644 --- a/src/flows/Flow.swift +++ b/src/flows/Flow.swift @@ -49,10 +49,6 @@ public enum DescopeFlowState: String { /// - SeeAlso: You can read more about Descope Flows on the [docs website](https://docs.descope.com/flows). @MainActor public class DescopeFlow { - /// Returns the ``DescopeFlow`` object for the current running flow or - /// `nil` if no flow is currently running. - public internal(set) static weak var current: DescopeFlow? - /// The URL where the flow is hosted. public let url: URL @@ -87,41 +83,6 @@ public class DescopeFlow { public init(url: URL) { self.url = url } - - /// Resumes a running flow that's waiting for Magic Link authentication. - /// - /// When a flow performs authentication with Magic Link at some point it will wait - /// for the user to receive an email and tap on the authentication URL provided inside. - /// The host application is expected to intercept this URL via Universal Links and - /// resume the running flow with it. - /// - /// You can do this by first getting a reference to the current running flow from - /// the ``DescopeFlow/current`` property and then calling the ``resume(with:)`` method - /// with the URL from the Universal Link. - /// - /// @main - /// struct MyApp: App { - /// // ... - /// - /// var body: some Scene { - /// WindowGroup { - /// ContentView().onOpenURL { url in - /// DescopeFlow.current?.resume(with: url) - /// } - /// } - /// } - /// } - public func resume(with url: URL) { - resume?(url) - } - - // Internal - - typealias ResumeClosure = @MainActor (URL) -> () - - /// While the flow is running this is set to a closure with a weak reference to - /// the ``DescopeFlowCoordinator`` to provide it with the resume URL. - var resume: ResumeClosure? } extension DescopeFlow: CustomStringConvertible { diff --git a/src/flows/FlowCoordinator.swift b/src/flows/FlowCoordinator.swift index 60982e5..991eede 100644 --- a/src/flows/FlowCoordinator.swift +++ b/src/flows/FlowCoordinator.swift @@ -53,9 +53,8 @@ public class DescopeFlowCoordinator { private var flow: DescopeFlow? { didSet { - oldValue?.resume = nil - flow?.resume = resumeClosure - logger = flow?.config.logger + sdk.resume = resumeClosure + logger = sdk.config.logger bridge.logger = logger } } @@ -63,11 +62,10 @@ public class DescopeFlowCoordinator { public func start(flow: DescopeFlow) { logger(.info, "Starting flow authentication", flow) #if DEBUG - precondition(flow.config.projectId != "", "The Descope singleton must be setup or an instance of DescopeSDK must be set on the flow") + precondition(sdk.config.projectId != "", "The Descope singleton must be setup or an instance of DescopeSDK must be set on the flow") #endif self.flow = flow - DescopeFlow.current = flow state = .started loadURL(flow.url) @@ -81,6 +79,10 @@ public class DescopeFlowCoordinator { webView?.load(request) } + private var sdk: DescopeSDK { + return flow?.descope ?? Descope.sdk + } + // State private func ensureState(_ states: DescopeFlowState...) -> Bool { @@ -98,13 +100,18 @@ public class DescopeFlowCoordinator { // Resume - private func resume(_ url: URL) { + private func resume(_ url: URL) -> Bool { + guard state == .ready else { + logger(.debug, "Ignoring resume URL", state) + return false + } logger(.info, "Received URL for resuming flow", url) sendResponse(.magicLink(url: url.absoluteString)) + return true } - private lazy var resumeClosure: DescopeFlow.ResumeClosure = { [weak self] url in - self?.resume(url) + private lazy var resumeClosure: DescopeSDK.ResumeClosure = { [weak self] url in + return self?.resume(url) ?? false } // Events @@ -117,10 +124,6 @@ public class DescopeFlowCoordinator { // keep its own state to ensure it only reports a single failure guard state != .failed else { return } - if DescopeFlow.current === flow { - DescopeFlow.current = nil - } - state = .failed let error = error as? DescopeError ?? DescopeError.flowFailed.with(cause: error) delegate?.coordinatorDidFailAuthentication(self, error: error) @@ -150,9 +153,6 @@ public class DescopeFlowCoordinator { Task { guard let authResponse = await parseAuthentication(data) else { return } guard ensureState(.ready) else { return } - if DescopeFlow.current === flow { - DescopeFlow.current = nil - } state = .finished delegate?.coordinatorDidFinishAuthentication(self, response: authResponse) } @@ -247,12 +247,6 @@ extension DescopeFlowCoordinator: FlowBridgeDelegate { } } -private extension DescopeFlow { - var config: DescopeConfig { - return descope?.config ?? Descope.sdk.config - } -} - private extension WKHTTPCookieStore { func cookies(for url: URL?) async -> [HTTPCookie] { return await allCookies().filter { cookie in diff --git a/src/sdk/SDK.swift b/src/sdk/SDK.swift index aeeb671..704e295 100644 --- a/src/sdk/SDK.swift +++ b/src/sdk/SDK.swift @@ -1,4 +1,6 @@ +import Foundation + /// Provides functions for working with the Descope API. /// /// The ``Descope`` singleton object exposes the same properties as the ``DescopeSDK`` class, @@ -96,6 +98,12 @@ public class DescopeSDK { self.init(config: config, client: DescopeClient(config: config)) } + /// Resumes an ongoing authentication that's waiting for Magic Link authentication. + @discardableResult @MainActor + public func handleURL(_ url: URL) -> Bool { + return resume(url) + } + // Internal /// The internal client used to perform API calls. @@ -120,6 +128,13 @@ public class DescopeSDK { self.sso = SSO(client: client) self.accessKey = AccessKey(client: client) } + + /// The type of the closure set in ``resume(with:)`` by SDK components. + typealias ResumeClosure = @MainActor (URL) -> (Bool) + + /// While the flow is running this is set to a closure with a weak reference to + /// the ``DescopeFlowCoordinator`` to provide it with the resume URL. + var resume: ResumeClosure = { _ in return false } } /// SDK information