Skip to content

Commit db7a094

Browse files
authored
feat(auth): add convenience deep link handling methods (#397)
* feat(auth): automatically load session from deep link * feat: add handle method * add handle url method to SupabaseClient
1 parent 77fb7e6 commit db7a094

File tree

5 files changed

+169
-15
lines changed

5 files changed

+169
-15
lines changed

Examples/Examples/Contants.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,18 @@
88
import Foundation
99

1010
enum Constants {
11-
static let redirectToURL = URL(string: "com.supabase.swift-examples://")!
11+
static let redirectToURL = URL(scheme: "com.supabase.swift-examples")!
12+
}
13+
14+
extension URL {
15+
init?(scheme: String) {
16+
var components = URLComponents()
17+
components.scheme = scheme
18+
19+
guard let url = components.url else {
20+
return nil
21+
}
22+
23+
self = url
24+
}
1225
}

Examples/Examples/ExamplesApp.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,41 @@ import GoogleSignIn
99
import Supabase
1010
import SwiftUI
1111

12+
class AppDelegate: UIResponder, UIApplicationDelegate {
13+
func application(
14+
_ application: UIApplication,
15+
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
16+
) -> Bool {
17+
if let url = launchOptions?[.url] as? URL {
18+
supabase.handle(url)
19+
}
20+
return true
21+
}
22+
23+
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
24+
supabase.handle(url)
25+
return true
26+
}
27+
28+
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
29+
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
30+
configuration.delegateClass = SceneDelegate.self
31+
return configuration
32+
}
33+
}
34+
35+
class SceneDelegate: UIResponder, UISceneDelegate {
36+
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
37+
guard let url = URLContexts.first?.url else { return }
38+
39+
supabase.handle(url)
40+
}
41+
}
42+
1243
@main
1344
struct ExamplesApp: App {
45+
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
46+
1447
var body: some Scene {
1548
WindowGroup {
1649
RootView()

Examples/Examples/Info.plist

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,20 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5-
<key>GIDClientID</key>
6-
<string>YOUR_IOS_CLIENT_ID</string>
7-
<key>GIDServerClientID</key>
8-
<string>YOUR_SERVER_CLIENT_ID</string>
95
<key>CFBundleURLTypes</key>
106
<array>
117
<dict>
128
<key>CFBundleTypeRole</key>
139
<string>Editor</string>
1410
<key>CFBundleURLSchemes</key>
1511
<array>
16-
<string>com.supabase.swift-examples</string>
17-
</array>
18-
</dict>
19-
<dict>
20-
<key>CFBundleTypeRole</key>
21-
<string>Editor</string>
22-
<key>CFBundleURLSchemes</key>
23-
<array>
24-
<string>YOUR_DOT_REVERSED_IOS_CLIENT_ID</string>
12+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
2513
</array>
2614
</dict>
2715
</array>
16+
<key>GIDClientID</key>
17+
<string>YOUR_IOS_CLIENT_ID</string>
18+
<key>GIDServerClientID</key>
19+
<string>YOUR_SERVER_CLIENT_ID</string>
2820
</dict>
2921
</plist>

Sources/Auth/AuthClient.swift

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public final class AuthClient: Sendable {
1717
private var date: @Sendable () -> Date { Current.date }
1818
private var sessionManager: SessionManager { Current.sessionManager }
1919
private var eventEmitter: AuthStateChangeEventEmitter { Current.eventEmitter }
20-
private var logger: (any SupabaseLogger)? { Current.logger }
20+
private var logger: (any SupabaseLogger)? { Current.configuration.logger }
2121
private var storage: any AuthLocalStorage { Current.configuration.localStorage }
2222

2323
/// Returns the session, refreshing it if necessary.
@@ -596,6 +596,67 @@ public final class AuthClient: Sendable {
596596
}
597597
#endif
598598

599+
/// Handles an incoming URL received by the app.
600+
///
601+
/// ## Usage example:
602+
///
603+
/// ### UIKit app lifecycle
604+
///
605+
/// In your `AppDelegate.swift`:
606+
///
607+
/// ```swift
608+
/// public func application(
609+
/// _ application: UIApplication,
610+
/// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
611+
/// ) -> Bool {
612+
/// if let url = launchOptions?[.url] as? URL {
613+
/// supabase.auth.handle(url)
614+
/// }
615+
///
616+
/// return true
617+
/// }
618+
///
619+
/// func application(
620+
/// _ app: UIApplication,
621+
/// open url: URL,
622+
/// options: [UIApplication.OpenURLOptionsKey: Any]
623+
/// ) -> Bool {
624+
/// supabase.auth.handle(url)
625+
/// return true
626+
/// }
627+
/// ```
628+
///
629+
/// ### UIKit app lifecycle with scenes
630+
///
631+
/// In your `SceneDelegate.swift`:
632+
///
633+
/// ```swift
634+
/// func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
635+
/// guard let url = URLContexts.first?.url else { return }
636+
/// supabase.auth.handle(url)
637+
/// }
638+
/// ```
639+
///
640+
/// ### SwiftUI app lifecycle
641+
///
642+
/// In your `AppDelegate.swift`:
643+
///
644+
/// ```swift
645+
/// SomeView()
646+
/// .onOpenURL { url in
647+
/// supabase.auth.handle(url)
648+
/// }
649+
/// ```
650+
public func handle(_ url: URL) {
651+
Task {
652+
do {
653+
try await session(from: url)
654+
} catch {
655+
logger?.error("Failure loading session from url '\(url)' error: \(error)")
656+
}
657+
}
658+
}
659+
599660
/// Gets the session data from a OAuth2 callback URL.
600661
@discardableResult
601662
public func session(from url: URL) async throws -> Session {

Sources/Supabase/SupabaseClient.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,61 @@ public final class SupabaseClient: Sendable {
254254
await realtimeV2.removeAllChannels()
255255
}
256256

257+
/// Handles an incoming URL received by the app.
258+
///
259+
/// ## Usage example:
260+
///
261+
/// ### UIKit app lifecycle
262+
///
263+
/// In your `AppDelegate.swift`:
264+
///
265+
/// ```swift
266+
/// public func application(
267+
/// _ application: UIApplication,
268+
/// didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
269+
/// ) -> Bool {
270+
/// if let url = launchOptions?[.url] as? URL {
271+
/// supabase.handle(url)
272+
/// }
273+
///
274+
/// return true
275+
/// }
276+
///
277+
/// func application(
278+
/// _ app: UIApplication,
279+
/// open url: URL,
280+
/// options: [UIApplication.OpenURLOptionsKey: Any]
281+
/// ) -> Bool {
282+
/// supabase.handle(url)
283+
/// return true
284+
/// }
285+
/// ```
286+
///
287+
/// ### UIKit app lifecycle with scenes
288+
///
289+
/// In your `SceneDelegate.swift`:
290+
///
291+
/// ```swift
292+
/// func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
293+
/// guard let url = URLContexts.first?.url else { return }
294+
/// supabase.handle(url)
295+
/// }
296+
/// ```
297+
///
298+
/// ### SwiftUI app lifecycle
299+
///
300+
/// In your `AppDelegate.swift`:
301+
///
302+
/// ```swift
303+
/// SomeView()
304+
/// .onOpenURL { url in
305+
/// supabase.handle(url)
306+
/// }
307+
/// ```
308+
public func handle(_ url: URL) {
309+
auth.handle(url)
310+
}
311+
257312
deinit {
258313
mutableState.listenForAuthEventsTask?.cancel()
259314
}

0 commit comments

Comments
 (0)