From 41dd4ed77cd7e64eabbe5508df47e7d1e23bddc3 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 15 May 2024 15:09:13 -0300 Subject: [PATCH 1/3] feat(auth): automatically load session from deep link --- Examples/Examples/ExamplesApp.swift | 27 +++++++++ ...SupabaseClient+UIApplicationDelegate.swift | 56 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index 9a70a4256..acf7172b6 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -9,8 +9,35 @@ import GoogleSignIn import Supabase import SwiftUI +class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + supabase.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + supabase.application(app, open: url, options: options) + } + + 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) { + supabase.scene(scene, openURLContexts: URLContexts) + } +} + @main struct ExamplesApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + var body: some Scene { WindowGroup { RootView() diff --git a/Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift b/Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift new file mode 100644 index 000000000..e56e791f7 --- /dev/null +++ b/Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift @@ -0,0 +1,56 @@ +// +// SupabaseClient+UIApplicationDelegate.swift +// +// +// Created by Guilherme Souza on 15/05/24. +// + +import UIKit + +extension SupabaseClient { + @discardableResult + public func application( + _: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + if let url = launchOptions?[.url] as? URL { + handleDeepLink(url) + } + + return true + } + + public func application( + _: UIApplication, + open url: URL, + options _: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + handleDeepLink(url) + return true + } + + @MainActor + public func scene(_: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + + handleDeepLink(url) + } + + private func handleDeepLink(_ url: URL) { + let logger = options.global.logger + + Task { + do { + try await auth.session(from: url) + } catch { + logger?.error( + """ + Failure loading session. + URL: \(url.absoluteString) + Error: \(error) + """ + ) + } + } + } +} From 48ae0af7566003467ae117f5cdf36c6d6a77ac2b Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 22 May 2024 08:50:34 -0300 Subject: [PATCH 2/3] feat: add handle method --- Examples/Examples/Contants.swift | 15 ++++- Examples/Examples/Info.plist | 18 ++---- Sources/Auth/AuthClient.swift | 63 ++++++++++++++++++- ...SupabaseClient+UIApplicationDelegate.swift | 56 ----------------- 4 files changed, 81 insertions(+), 71 deletions(-) delete mode 100644 Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift 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/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+UIApplicationDelegate.swift b/Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift deleted file mode 100644 index e56e791f7..000000000 --- a/Sources/Supabase/SupabaseClient+UIApplicationDelegate.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SupabaseClient+UIApplicationDelegate.swift -// -// -// Created by Guilherme Souza on 15/05/24. -// - -import UIKit - -extension SupabaseClient { - @discardableResult - public func application( - _: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - if let url = launchOptions?[.url] as? URL { - handleDeepLink(url) - } - - return true - } - - public func application( - _: UIApplication, - open url: URL, - options _: [UIApplication.OpenURLOptionsKey: Any] = [:] - ) -> Bool { - handleDeepLink(url) - return true - } - - @MainActor - public func scene(_: UIScene, openURLContexts URLContexts: Set) { - guard let url = URLContexts.first?.url else { return } - - handleDeepLink(url) - } - - private func handleDeepLink(_ url: URL) { - let logger = options.global.logger - - Task { - do { - try await auth.session(from: url) - } catch { - logger?.error( - """ - Failure loading session. - URL: \(url.absoluteString) - Error: \(error) - """ - ) - } - } - } -} From 91daac507959156bff7c26743f04be93e8a6cb93 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Wed, 22 May 2024 09:34:26 -0300 Subject: [PATCH 3/3] add handle url method to SupabaseClient --- Examples/Examples/ExamplesApp.swift | 12 ++++-- Sources/Supabase/SupabaseClient.swift | 55 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index acf7172b6..b9d096359 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -14,11 +14,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil ) -> Bool { - supabase.application(application, didFinishLaunchingWithOptions: launchOptions) + 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.application(app, open: url, options: options) + supabase.handle(url) + return true } func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { @@ -30,7 +34,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { class SceneDelegate: UIResponder, UISceneDelegate { func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { - supabase.scene(scene, openURLContexts: URLContexts) + guard let url = URLContexts.first?.url else { return } + + supabase.handle(url) } } 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() }