Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add handleURL function to replace flow resume #79

Merged
merged 1 commit into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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