diff --git a/Examples/Examples/Contants.swift b/Examples/Examples/Contants.swift index b44370191..1891770ce 100644 --- a/Examples/Examples/Contants.swift +++ b/Examples/Examples/Contants.swift @@ -8,5 +8,18 @@ import Foundation enum Constants { - static let redirectToURL = URL(string: "com.supabase.swift-examples://")! + static let redirectToURL = URL(scheme: "com.supabase.swift-examples")! +} + +extension URL { + init?(scheme: String) { + var components = URLComponents() + components.scheme = scheme + + guard let url = components.url else { + return nil + } + + self = url + } } diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index 9a70a4256..b9d096359 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -9,8 +9,41 @@ import GoogleSignIn import Supabase import SwiftUI +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + if let url = launchOptions?[.url] as? URL { + supabase.handle(url) + } + return true + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + supabase.handle(url) + return true + } + + func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { + let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role) + configuration.delegateClass = SceneDelegate.self + return configuration + } +} + +class SceneDelegate: UIResponder, UISceneDelegate { + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + + supabase.handle(url) + } +} + @main struct ExamplesApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var body: some Scene { WindowGroup { RootView() diff --git a/Examples/Examples/Info.plist b/Examples/Examples/Info.plist index f674da160..8070cab29 100644 --- a/Examples/Examples/Info.plist +++ b/Examples/Examples/Info.plist @@ -2,10 +2,6 @@ - GIDClientID - YOUR_IOS_CLIENT_ID - GIDServerClientID - YOUR_SERVER_CLIENT_ID CFBundleURLTypes @@ -13,17 +9,13 @@ Editor CFBundleURLSchemes - com.supabase.swift-examples - - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - YOUR_DOT_REVERSED_IOS_CLIENT_ID + $(PRODUCT_BUNDLE_IDENTIFIER) + GIDClientID + YOUR_IOS_CLIENT_ID + GIDServerClientID + YOUR_SERVER_CLIENT_ID diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 9b1a545de..87d757549 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -17,7 +17,7 @@ public final class AuthClient: Sendable { private var date: @Sendable () -> Date { Current.date } private var sessionManager: SessionManager { Current.sessionManager } private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter } - private var logger: (any SupabaseLogger)? { Current.logger } + private var logger: (any SupabaseLogger)? { Current.configuration.logger } private var storage: any AuthLocalStorage { Current.configuration.localStorage } /// Returns the session, refreshing it if necessary. @@ -596,6 +596,67 @@ public final class AuthClient: Sendable { } #endif + /// Handles an incoming URL received by the app. + /// + /// ## Usage example: + /// + /// ### UIKit app lifecycle + /// + /// In your `AppDelegate.swift`: + /// + /// ```swift + /// public func application( + /// _ application: UIApplication, + /// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + /// ) -> Bool { + /// if let url = launchOptions?[.url] as? URL { + /// supabase.auth.handle(url) + /// } + /// + /// return true + /// } + /// + /// func application( + /// _ app: UIApplication, + /// open url: URL, + /// options: [UIApplication.OpenURLOptionsKey: Any] + /// ) -> Bool { + /// supabase.auth.handle(url) + /// return true + /// } + /// ``` + /// + /// ### UIKit app lifecycle with scenes + /// + /// In your `SceneDelegate.swift`: + /// + /// ```swift + /// func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + /// guard let url = URLContexts.first?.url else { return } + /// supabase.auth.handle(url) + /// } + /// ``` + /// + /// ### SwiftUI app lifecycle + /// + /// In your `AppDelegate.swift`: + /// + /// ```swift + /// SomeView() + /// .onOpenURL { url in + /// supabase.auth.handle(url) + /// } + /// ``` + public func handle(_ url: URL) { + Task { + do { + try await session(from: url) + } catch { + logger?.error("Failure loading session from url '\(url)' error: \(error)") + } + } + } + /// Gets the session data from a OAuth2 callback URL. @discardableResult public func session(from url: URL) async throws -> Session { diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index f06c84109..228280356 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -254,6 +254,61 @@ public final class SupabaseClient: Sendable { await realtimeV2.removeAllChannels() } + /// Handles an incoming URL received by the app. + /// + /// ## Usage example: + /// + /// ### UIKit app lifecycle + /// + /// In your `AppDelegate.swift`: + /// + /// ```swift + /// public func application( + /// _ application: UIApplication, + /// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + /// ) -> Bool { + /// if let url = launchOptions?[.url] as? URL { + /// supabase.handle(url) + /// } + /// + /// return true + /// } + /// + /// func application( + /// _ app: UIApplication, + /// open url: URL, + /// options: [UIApplication.OpenURLOptionsKey: Any] + /// ) -> Bool { + /// supabase.handle(url) + /// return true + /// } + /// ``` + /// + /// ### UIKit app lifecycle with scenes + /// + /// In your `SceneDelegate.swift`: + /// + /// ```swift + /// func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + /// guard let url = URLContexts.first?.url else { return } + /// supabase.handle(url) + /// } + /// ``` + /// + /// ### SwiftUI app lifecycle + /// + /// In your `AppDelegate.swift`: + /// + /// ```swift + /// SomeView() + /// .onOpenURL { url in + /// supabase.handle(url) + /// } + /// ``` + public func handle(_ url: URL) { + auth.handle(url) + } + deinit { mutableState.listenForAuthEventsTask?.cancel() }