Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class SignInViewController: UIViewController {
}

@IBAction func signIn(_ sender: Any) {
signInButton.isEnabled = false
let window = viewIfLoaded?.window
auth?.signIn(from: window) { result in
switch result {
Expand All @@ -72,6 +73,10 @@ class SignInViewController: UIViewController {
case .failure(let error):
self.show(error: error)
}

Task { @MainActor in
self.signInButton.isEnabled = true
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ struct SignInView: View {
@State var signInError: (any Error)?
@State var hasError: Bool = false
@State var clientId: String?
@State var state: SignInState = .unconfigured

enum SignInState: Sendable {
case unconfigured
case ready
case signingIn
case signedIn
}

init(signedIn: Binding<Bool>) {
self._signedIn = signedIn
Expand All @@ -46,6 +54,7 @@ struct SignInView: View {

Button("Sign In") {
guard let auth = BrowserSignin.shared else { return }
state = .signingIn
auth.ephemeralSession = ephemeralSession
Task {
do {
Expand All @@ -58,7 +67,7 @@ struct SignInView: View {
}
}
}
.disabled(clientId == nil)
.disabled(state != .ready)
.alert(isPresented: $hasError) {
Alert(
title: Text("Error"),
Expand All @@ -78,8 +87,9 @@ struct SignInView: View {
.padding()
}
.onAppear {
Task {
clientId = await BrowserSignin.shared?.signInFlow.client.configuration.clientId
if let configuration = BrowserSignin.shared?.signInFlow.client.configuration {
clientId = configuration.clientId
state = .ready
Comment thread
AlexNachbaur marked this conversation as resolved.
}
}
}
Expand All @@ -93,9 +103,11 @@ struct SignInView: View {
let token = try await auth.signIn(from: ASPresentationAnchor())
try Credential.store(token)
signedIn = true
state = .signedIn
} catch {
signInError = error
signedIn = false
state = .ready
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions Sources/BrowserSignin/BrowserSignin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public enum BrowserSigninError: Error {
case noAuthenticatorProviderResonse
case serverError(_ error: OAuth2ServerError)
case invalidRedirectScheme(_ scheme: String?)
case userCancelledLogin
case userCancelledLogin(_ reason: String? = nil)
case missingIdToken
case oauth2(error: OAuth2Error)
case generic(error: any Error)
Expand All @@ -49,7 +49,11 @@ public enum BrowserSigninError: Error {
///
/// To customize the authentication flow, please read more about the underlying OAuth2 client within the OAuth2Auth library, and how that relates to the ``signInFlow`` or ``signOutFlow`` properties.
///
/// > Important: If your application targets iOS 9.x-10.x, you should add the redirect URI for your client configuration to your app's supported URL schemes. This is because users on devices older than iOS 11 will be prompted to sign in using `SFSafariViewController`, which does not allow your application to detect the final token redirect.
/// ## Redirect URI Support
///
/// This library supports both custom URIs and HTTPS redirect URLs on supporting platforms and iOS versions. This requires that your application is configured to use associated domains, with the application's identifier included in the associated domain's `webcredentials` list.
///
/// > Note: The use of HTTPS addresses within the redirect callback URI is limited by the availability of support within ASWebAuthenticationSession, which currently requires a minimum of iOS 17.4, macOS 14.4, watchOS 10.4, tvOS 17.4, or visionOS 1.1.
@MainActor
@available(iOS 13.0, macOS 10.15, tvOS 16.0, watchOS 7.0, visionOS 1.0, macCatalyst 13.0, *)
public final class BrowserSignin {
Expand Down
21 changes: 15 additions & 6 deletions Sources/BrowserSignin/Internal/BrowserSigninError+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ extension BrowserSigninError: LocalizedError {
if nsError.domain == ASWebAuthenticationSessionErrorDomain,
nsError.code == ASWebAuthenticationSessionError.canceledLogin.rawValue
{
self = .userCancelledLogin
self = .userCancelledLogin(nsError.localizedFailureReason)
} else if let error = error as? OAuth2Error {
self = .oauth2(error: error)
} else if let error = error as? OAuth2ServerError {
Expand Down Expand Up @@ -65,11 +65,20 @@ extension BrowserSigninError: LocalizedError {
bundle: .browserSignin,
comment: ""))

case .userCancelledLogin:
return NSLocalizedString("user_cancelled_login_description",
tableName: "BrowserSignin",
bundle: .browserSignin,
comment: "")
case .userCancelledLogin(let reason):
if let reason {
return String.localizedStringWithFormat(
NSLocalizedString("user_cancelled_login_reason_description",
tableName: "BrowserSignin",
bundle: .browserSignin,
comment: ""),
reason)
} else {
return NSLocalizedString("user_cancelled_login_description",
tableName: "BrowserSignin",
bundle: .browserSignin,
comment: "")
}

case .missingIdToken:
return NSLocalizedString("missing_id_token_description",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import AuthenticationServices
@available(iOS 12.0, macCatalyst 13.0, macOS 10.15, tvOS 16.0, visionOS 1.0, watchOS 6.2, *)
protocol AuthenticationServicesProviderSession: NSObjectProtocol, Sendable {
init(url URL: URL, callbackURLScheme: String?, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler)

@available(iOS 17.4, macOS 14.4, watchOS 10.4, tvOS 17.4, visionOS 1.1, *)
init(url URL: URL, callback: ASWebAuthenticationSession.Callback, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler)

@available(iOS 13.4, macCatalyst 13.4, macOS 10.15.4, watchOS 6.2, visionOS 1.0, tvOS 16.0, *)
var canStart: Bool { get }
Expand Down Expand Up @@ -73,18 +76,42 @@ final class AuthenticationServicesProvider: NSObject, BrowserSigninProvider {

super.init()
}


func createSession(authorizeUrl url: URL, callbackURL: URL, completionHandler: @escaping ASWebAuthenticationSession.CompletionHandler) -> (any AuthenticationServicesProviderSession) {
Comment thread
AlexNachbaur marked this conversation as resolved.
if #available(iOS 17.4, macOS 14.4, watchOS 10.4, tvOS 17.4, visionOS 1.1, *) {
if let scheme = callbackURL.scheme {
let callback: ASWebAuthenticationSession.Callback?
if scheme == "https",
let host = callbackURL.host
{
callback = .https(host: host, path: callbackURL.path)
} else {
callback = .customScheme(scheme)
}

if let callback {
return Self.authenticationSessionClass.init(
url: url,
callback: callback,
completionHandler: completionHandler)
}
}
}

return Self.authenticationSessionClass.init(
url: url,
callbackURLScheme: callbackURL.scheme,
completionHandler: completionHandler)
}

@MainActor
func open(authorizeUrl: URL, redirectUri: URL) async throws -> URL {
return try await withCheckedThrowingContinuation { continuation in
let session = Self.authenticationSessionClass.init(
url: authorizeUrl,
callbackURLScheme: redirectUri.scheme,
completionHandler: { url, error in
continuation.resume(with: self.process(redirectUri: redirectUri,
url: url,
error: error))
})
let session = createSession(authorizeUrl: authorizeUrl, callbackURL: redirectUri) { url, error in
continuation.resume(with: self.process(redirectUri: redirectUri,
url: url,
error: error))
}

#if !os(watchOS) && !os(tvOS)
session.presentationContextProvider = self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"authentication_provider_error" = "Could not set up a provider for handling web authentication.";
"invalid_redirect_scheme_description" = "Cannot resume sign in with an invalid URL scheme: %@.";
"user_cancelled_login_description" = "Authentication cancelled by the user.";
"user_cancelled_login_reason_description" = "Authentication cancelled by the user: %@.";
"generic_description" = "Authentication error: %@";
"unknown_error_message" = "Unknown error";
"no_scheme_defined" = "No URL scheme defined";
Expand Down