Skip to content

Commit 7dfaa46

Browse files
authored
feat(auth): add linkIdentity method (#392)
1 parent 76233bb commit 7dfaa46

File tree

6 files changed

+127
-5
lines changed

6 files changed

+127
-5
lines changed

Examples/Examples/Profile/UserIdentityList.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@ struct UserIdentityList: View {
6262
Button(provider.rawValue) {
6363
Task {
6464
do {
65-
let response = try await supabase.auth.getLinkIdentityURL(provider: provider)
66-
openURL(response.url)
67-
debug("getLinkIdentityURL: \(response.url) opened for provider \(response.provider)")
65+
try await supabase.auth.linkIdentity(provider: provider)
6866
} catch {
6967
self.error = error
7068
}

Sources/Auth/AuthClient.swift

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -955,14 +955,67 @@ public final class AuthClient: Sendable {
955955
try await user().identities ?? []
956956
}
957957

958+
/// Links an OAuth identity to an existing user.
959+
///
960+
/// This method supports the PKCE flow.
961+
///
962+
/// - Parameters:
963+
/// - provider: The provider you want to link the user with.
964+
/// - scopes: A space-separated list of scopes granted to the OAuth application.
965+
/// - redirectTo: A URL to send the user to after they are confirmed.
966+
/// - queryParams: Additional query parameters to use.
967+
/// - launchURL: Custom launch URL logic.
968+
public func linkIdentity(
969+
provider: Provider,
970+
scopes: String? = nil,
971+
redirectTo: URL? = nil,
972+
queryParams: [(name: String, value: String?)] = [],
973+
launchURL: @MainActor (_ url: URL) -> Void
974+
) async throws {
975+
let response = try await getLinkIdentityURL(
976+
provider: provider,
977+
scopes: scopes,
978+
redirectTo: redirectTo,
979+
queryParams: queryParams
980+
)
981+
982+
await launchURL(response.url)
983+
}
984+
985+
/// Links an OAuth identity to an existing user.
986+
///
987+
/// This method supports the PKCE flow.
988+
///
989+
/// - Parameters:
990+
/// - provider: The provider you want to link the user with.
991+
/// - scopes: A space-separated list of scopes granted to the OAuth application.
992+
/// - redirectTo: A URL to send the user to after they are confirmed.
993+
/// - queryParams: Additional query parameters to use.
994+
///
995+
/// - Note: This method opens the URL using the default URL opening mechanism for the platform, if you with to provide your own URL opening logic use ``linkIdentity(provider:scopes:redirectTo:queryParams:launchURL:)``.
996+
public func linkIdentity(
997+
provider: Provider,
998+
scopes: String? = nil,
999+
redirectTo: URL? = nil,
1000+
queryParams: [(name: String, value: String?)] = []
1001+
) async throws {
1002+
try await linkIdentity(
1003+
provider: provider,
1004+
scopes: scopes,
1005+
redirectTo: redirectTo,
1006+
queryParams: queryParams,
1007+
launchURL: { Current.urlOpener.open($0) }
1008+
)
1009+
}
1010+
9581011
/// Returns the URL to link the user's identity with an OAuth provider.
9591012
///
9601013
/// This method supports the PKCE flow.
9611014
///
9621015
/// - Parameters:
9631016
/// - provider: The provider you want to link the user with.
964-
/// - scopes: The scopes to request from the OAuth provider.
965-
/// - redirectTo: The redirect URL to use, specify a configured deep link.
1017+
/// - scopes: A space-separated list of scopes granted to the OAuth application.
1018+
/// - redirectTo: A URL to send the user to after they are confirmed.
9661019
/// - queryParams: Additional query parameters to use.
9671020
public func getLinkIdentityURL(
9681021
provider: Provider,

Sources/Auth/Internal/Dependencies.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct Dependencies: Sendable {
1212
var eventEmitter: AuthStateChangeEventEmitter = .shared
1313
var date: @Sendable () -> Date = { Date() }
1414
var codeVerifierStorage = CodeVerifierStorage.live
15+
var urlOpener: URLOpener = .live
1516

1617
var encoder: JSONEncoder { configuration.encoder }
1718
var decoder: JSONDecoder { configuration.decoder }

Sources/Auth/Internal/URLOpener.swift

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//
2+
// URLOpener.swift
3+
//
4+
//
5+
// Created by Guilherme Souza on 17/05/24.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(WatchKit)
11+
import WatchKit
12+
#endif
13+
14+
#if canImport(UIKit)
15+
import UIKit
16+
#endif
17+
18+
#if canImport(AppKit)
19+
import AppKit
20+
#endif
21+
22+
struct URLOpener {
23+
var open: @MainActor @Sendable (_ url: URL) -> Void
24+
}
25+
26+
extension URLOpener {
27+
static var live: Self {
28+
URLOpener { url in
29+
#if os(macOS)
30+
NSWorkspace.shared.open(url)
31+
#elseif os(iOS) || os(tvOS) || os(visionOS) || targetEnvironment(macCatalyst)
32+
UIApplication.shared.open(url)
33+
#elseif os(watchOS)
34+
WKExtension.shared().openSystemURL(url)
35+
#endif
36+
}
37+
}
38+
}

Tests/AuthTests/AuthClientTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,30 @@ final class AuthClientTests: XCTestCase {
298298
)
299299
}
300300

301+
func testLinkIdentity() async throws {
302+
let url = "https://github.com/login/oauth/authorize?client_id=1234&redirect_to=com.supabase.swift-examples://&redirect_uri=http://127.0.0.1:54321/auth/v1/callback&response_type=code&scope=user:email&skip_http_redirect=true&state=jwt"
303+
let sut = makeSUT { _ in
304+
.stub(
305+
"""
306+
{
307+
"url" : "\(url)"
308+
}
309+
"""
310+
)
311+
}
312+
313+
try storage.storeSession(.init(session: .validSession))
314+
315+
let receivedURL = LockIsolated<URL?>(nil)
316+
Current.urlOpener.open = { url in
317+
receivedURL.setValue(url)
318+
}
319+
320+
try await sut.linkIdentity(provider: .github)
321+
322+
XCTAssertEqual(receivedURL.value?.absoluteString, url)
323+
}
324+
301325
private func makeSUT(
302326
fetch: ((URLRequest) async throws -> HTTPResponse)? = nil
303327
) -> AuthClient {

Tests/IntegrationTests/AuthClientIntegrationTests.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,14 @@ final class AuthClientIntegrationTests: XCTestCase {
210210
}
211211
}
212212

213+
func testLinkIdentity() async throws {
214+
try await signUpIfNeededOrSignIn(email: mockEmail(), password: mockPassword())
215+
216+
try await authClient.linkIdentity(provider: .github) { url in
217+
XCTAssertTrue(url.absoluteString.contains("github.com"))
218+
}
219+
}
220+
213221
@discardableResult
214222
private func signUpIfNeededOrSignIn(
215223
email: String,

0 commit comments

Comments
 (0)