Skip to content

Commit

Permalink
Add handleURL function to replace flow resume
Browse files Browse the repository at this point in the history
  • Loading branch information
shilgapira committed Dec 29, 2024
1 parent ccab36c commit 6498894
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 63 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
```

Expand Down
52 changes: 52 additions & 0 deletions src/DescopeKit.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
}
39 changes: 0 additions & 39 deletions src/flows/Flow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand Down
36 changes: 15 additions & 21 deletions src/flows/FlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,21 +53,19 @@ 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
}
}

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)
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/sdk/SDK.swift
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit 6498894

Please sign in to comment.