From 07e2e24e10036187339741d7c83be6ce455a2f48 Mon Sep 17 00:00:00 2001 From: Sudhanshu Vohra Date: Fri, 17 Apr 2026 18:01:54 +0530 Subject: [PATCH 1/5] Incomplete error message issue fixed on Passkey registration failure --- .../Passkeys/Presentation/PasskeysEnrollmentViewModel.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift b/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift index c5917d3..fbf331c 100644 --- a/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift +++ b/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift @@ -79,7 +79,7 @@ final class PasskeysEnrollmentViewModel: NSObject, enrollPasskey() } catch { showLoader = false - errorViewModel = Auth0UIComponentError.unknown().errorViewModel { [weak self] in + errorViewModel = Auth0UIComponentError.unknown(message: error.localizedDescription).errorViewModel { [weak self] in Task { await self?.startEnrollment() } } } From 24e652c71e0e4ecd4495ff97ff8e6cbe89dcc6dd Mon Sep 17 00:00:00 2001 From: Sudhanshu96 Date: Mon, 27 Apr 2026 11:43:10 +0530 Subject: [PATCH 2/5] Changed the onChange callback to onReceive as the later best works with @Published publisher cause it guarantees response to a value change independent of SwiftUI's rendering pipeline (#19) --- AppUIComponents/LoginOptions/LoginOptionsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AppUIComponents/LoginOptions/LoginOptionsView.swift b/AppUIComponents/LoginOptions/LoginOptionsView.swift index 292cd1d..b653924 100644 --- a/AppUIComponents/LoginOptions/LoginOptionsView.swift +++ b/AppUIComponents/LoginOptions/LoginOptionsView.swift @@ -85,8 +85,8 @@ struct LoginOptionsView: View { showAlert = true } } - .onChange(of: viewModel.navigationRoute) { _ in - guard let route = viewModel.navigationRoute else { return } + .onReceive(viewModel.$navigationRoute) { route in + guard let route = route else { return } router.navigate(to: route) } } From b349662b9408e49a438a6707234ade8139531638 Mon Sep 17 00:00:00 2001 From: Sudhanshu96 Date: Mon, 27 Apr 2026 12:18:03 +0530 Subject: [PATCH 3/5] Step-up flow fixed (#20) * Bug fixed in Step-up flow fixed. Missing scope being passed causing the flow to trigger recursively * Failing test case fixed * Added documentation for scope related changes --- .../Core/Utils/ErrorHandler.swift | 6 ++++-- .../CredentialsManager+TokenProviderExtension.swift | 7 ++++--- Auth0UniversalComponents/TokenProvider.swift | 11 ++++++----- .../Mocks/MockTokenProvider.swift | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Auth0UniversalComponents/Core/Utils/ErrorHandler.swift b/Auth0UniversalComponents/Core/Utils/ErrorHandler.swift index 34f53dd..d8e0f0f 100644 --- a/Auth0UniversalComponents/Core/Utils/ErrorHandler.swift +++ b/Auth0UniversalComponents/Core/Utils/ErrorHandler.swift @@ -104,7 +104,8 @@ struct ErrorHandler { handler.showLoader = false dependencies.tokenProvider.store( apiCredentials: APICredentials(from: credentials), - for: dependencies.audience + for: dependencies.audience, + andScope: scope ) retryCallback() } catch { @@ -141,7 +142,8 @@ struct ErrorHandler { .start() dependencies.tokenProvider.store( apiCredentials: APICredentials(from: credentials), - for: dependencies.audience + for: dependencies.audience, + andScope: scope ) retryCallback() } catch { diff --git a/Auth0UniversalComponents/CredentialsManager+TokenProviderExtension.swift b/Auth0UniversalComponents/CredentialsManager+TokenProviderExtension.swift index fec9991..f34d3b4 100644 --- a/Auth0UniversalComponents/CredentialsManager+TokenProviderExtension.swift +++ b/Auth0UniversalComponents/CredentialsManager+TokenProviderExtension.swift @@ -22,13 +22,14 @@ extension CredentialsManager: TokenProvider { _ = store(credentials: credentials) } - /// Stores API credentials for a specific audience in the credentials manager. + /// Stores API credentials for a specific audience and scope in the credentials manager. /// /// - Parameters: /// - apiCredentials: The API credentials to store /// - audience: The API audience (e.g., "https://api.example.com") - public func store(apiCredentials: APICredentials, for audience: String) { - _ = store(apiCredentials: apiCredentials, forAudience: audience) + /// - scope: The scopes associated with the credentials (space-separated) + public func store(apiCredentials: APICredentials, for audience: String, andScope scope: String) { + _ = store(apiCredentials: apiCredentials, forAudience: audience, forScope: scope) } /// Fetches API credentials for a specific audience and scope from the credentials manager. diff --git a/Auth0UniversalComponents/TokenProvider.swift b/Auth0UniversalComponents/TokenProvider.swift index a271efe..c218ced 100644 --- a/Auth0UniversalComponents/TokenProvider.swift +++ b/Auth0UniversalComponents/TokenProvider.swift @@ -24,18 +24,18 @@ import Combine /// try? credentialsManager.store(credentials: credentials) /// } /// -/// func store(apiCredentials: APICredentials, for audience: String) { -/// apiCredentialsCache[audience] = apiCredentials +/// func store(apiCredentials: APICredentials, for audience: String, andScope scope: String) { +/// apiCredentialsCache["\(audience)_\(scope)"] = apiCredentials /// } /// /// func fetchAPICredentials(audience: String, scope: String) async throws -> APICredentials { -/// if let cached = apiCredentialsCache[audience] { +/// if let cached = apiCredentialsCache["\(audience)_\(scope)"] { /// return cached /// } /// // Fetch new API credentials /// let credentials = try await Auth0.authentication() /// .credentials(forAudience: audience, scope: scope) -/// store(apiCredentials: credentials, for: audience) +/// store(apiCredentials: credentials, for: audience, andScope: scope) /// return credentials /// } /// } @@ -59,7 +59,8 @@ public protocol TokenProvider: Sendable { /// - Parameters: /// - apiCredentials: The API credentials to store /// - audience: The API audience (e.g., "https://api.example.com") - func store(apiCredentials: APICredentials, for audience: String) + /// - scope: The scopes associated with the credentials (space-separated) + func store(apiCredentials: APICredentials, for audience: String, andScope scope: String) /// Fetches API credentials for a specific audience and scope. /// diff --git a/Auth0UniversalComponentsTests/Mocks/MockTokenProvider.swift b/Auth0UniversalComponentsTests/Mocks/MockTokenProvider.swift index 30817db..9c6c85d 100644 --- a/Auth0UniversalComponentsTests/Mocks/MockTokenProvider.swift +++ b/Auth0UniversalComponentsTests/Mocks/MockTokenProvider.swift @@ -10,7 +10,7 @@ struct MockTokenProvider: TokenProvider { func storeCredentials(credentials: Credentials) {} - func store(apiCredentials: APICredentials, for audience: String) {} + func store(apiCredentials: APICredentials, for audience: String, andScope scope: String) {} func fetchAPICredentials(audience: String, scope: String) async throws -> APICredentials { APICredentials(accessToken: "mock-access-token", tokenType: "Bearer", expiresIn: Date(), scope: "openid profile offline_access") From fa4447ae0e872c785f58c1562fc8ca4b35c0e1b4 Mon Sep 17 00:00:00 2001 From: Sudhanshu96 Date: Mon, 27 Apr 2026 12:22:53 +0530 Subject: [PATCH 4/5] Fixed bugs (#18) * Fixed the following bugs: 1. Clear web cache on logout. 2. UI bug of logout button overlapping profile options on small screen at Welcome page. * Added descriptive comment --- .../LoginOptions/LoginOptionsView.swift | 3 --- AppUIComponents/Welcome/WelcomeView.swift | 21 ++++++++++++------- .../Welcome/WelcomeViewModel.swift | 20 ++++++++++++++---- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/AppUIComponents/LoginOptions/LoginOptionsView.swift b/AppUIComponents/LoginOptions/LoginOptionsView.swift index b653924..1db609b 100644 --- a/AppUIComponents/LoginOptions/LoginOptionsView.swift +++ b/AppUIComponents/LoginOptions/LoginOptionsView.swift @@ -92,7 +92,6 @@ struct LoginOptionsView: View { } // MARK: - Loading Overlay - private var loadingOverlay: some View { Color.black.opacity(0.25) .ignoresSafeArea() @@ -247,8 +246,6 @@ struct LoginOptionsView: View { .contentShape(Rectangle()) .padding(.all, theme.spacing.md) } - - } extension LoginOptionsView { diff --git a/AppUIComponents/Welcome/WelcomeView.swift b/AppUIComponents/Welcome/WelcomeView.swift index f212c8b..62dbfd8 100644 --- a/AppUIComponents/Welcome/WelcomeView.swift +++ b/AppUIComponents/Welcome/WelcomeView.swift @@ -20,15 +20,17 @@ struct WelcomeView: View { // MARK: - Main body var body: some View { - VStack(alignment: .center, spacing: theme.spacing.lg) { + VStack(alignment: .center, spacing: theme.spacing.md) { headerView() makeAvailableOptionsList() logoutButton() + + Spacer() } - .padding(theme.spacing.xl) + .padding(.horizontal, theme.spacing.xl) .padding(.top, theme.spacing.xxl) #if !os(macOS) .navigationBarBackButtonHidden() @@ -59,19 +61,22 @@ struct WelcomeView: View { GridItem(.flexible()) ] - ScrollView { + ScrollView(showsIndicators: false) { LazyVGrid(columns: columns, spacing: theme.spacing.md) { - ForEach($viewModel.options) { item in - optionTile(for: item) + ForEach(viewModel.options.indices, id: \.self) { index in + let isLastRow = index / 2 == (viewModel.options.count - 1) / 2 + optionTile(for: $viewModel.options[index], isLastRow: isLastRow) } } + .padding(theme.spacing.xxs) } + .fixedSize(horizontal: false, vertical: true) .onPreferenceChange(TileHeightKey.self) { tileHeight = $0 } } // MARK: - Option Tile @ViewBuilder - private func optionTile(for item: Binding) -> some View { + private func optionTile(for item: Binding, isLastRow: Bool = false) -> some View { let isAvailable = item.route.wrappedValue != nil VStack(alignment: .leading, spacing: 0) { @@ -102,7 +107,7 @@ struct WelcomeView: View { .inset(by: 0.5) .stroke(theme.colors.border.regular, lineWidth: 1) } - .shadow(color: .black.opacity(0.08), radius: 6, x: 0, y: 2) + .shadow(color: .black.opacity(0.04), radius: 3, x: 0, y: 1) .opacity(isAvailable ? 1 : 0.4) .disabled(!isAvailable) .onTapGesture { @@ -127,7 +132,7 @@ struct WelcomeView: View { .font(.system(size: 16, weight: .semibold)) .foregroundStyle(Color("262420", bundle: .main)) .frame(maxWidth: .infinity, alignment: .center) - .padding(.vertical, 14) + .frame(height: theme.sizes.buttonHeight) } } } diff --git a/AppUIComponents/Welcome/WelcomeViewModel.swift b/AppUIComponents/Welcome/WelcomeViewModel.swift index fde5756..61670cf 100644 --- a/AppUIComponents/Welcome/WelcomeViewModel.swift +++ b/AppUIComponents/Welcome/WelcomeViewModel.swift @@ -27,10 +27,22 @@ class WelcomeViewModel: ObservableObject { /// Method to perform logout func performLogout(withCompletion completion: @escaping () -> Void) { - - _ = credentialsManager.clear() - - completion() + // Clear the web browser cookies + Auth0.webAuth() + .clearSession(federated: false) { [weak self] result in + switch result { + case .success(_): + + // Clears the crendtials stored in keychain after the web session is successfully cleared + _ = self?.credentialsManager.clear() + + completion() + break + case .failure(let error): + debugPrint("Error: \(error.localizedDescription)") + break + } + } } /// Method to fetch available options From fc935496229103ddca8b003f13250aaa07bfce0b Mon Sep 17 00:00:00 2001 From: Sudhanshu Vohra Date: Mon, 27 Apr 2026 11:54:56 +0530 Subject: [PATCH 5/5] Passkeys implementation issues fixed --- .../LoginOptions/LoginOptionsViewModel.swift | 2 +- .../MyAccountAuthMethodsViewModel.swift | 1 + .../PasskeysEnrollmentViewModel.swift | 28 ++++++++++++------- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/AppUIComponents/LoginOptions/LoginOptionsViewModel.swift b/AppUIComponents/LoginOptions/LoginOptionsViewModel.swift index 5290f7f..1cc30db 100644 --- a/AppUIComponents/LoginOptions/LoginOptionsViewModel.swift +++ b/AppUIComponents/LoginOptions/LoginOptionsViewModel.swift @@ -37,7 +37,7 @@ final class LoginOptionsViewModel: ObservableObject { return (isAuthenticated != nil) ? .welcome : nil } catch { - debugPrint("Check authentication failed!!!") + debugPrint("Check authentication failed!!!. Error: \(error)") return nil } } diff --git a/Auth0UniversalComponents/AuthMethodsHome/Presentation/MyAccountAuthMethodsViewModel.swift b/Auth0UniversalComponents/AuthMethodsHome/Presentation/MyAccountAuthMethodsViewModel.swift index 27ccbcb..e244cf7 100644 --- a/Auth0UniversalComponents/AuthMethodsHome/Presentation/MyAccountAuthMethodsViewModel.swift +++ b/Auth0UniversalComponents/AuthMethodsHome/Presentation/MyAccountAuthMethodsViewModel.swift @@ -151,6 +151,7 @@ final class MyAccountAuthMethodsViewModel: ObservableObject, ErrorViewModelHandl self.viewComponents = [.emptyFactors] } } catch { + debugPrint("Error occured in My Account Auth View: \(error)") await handle(error: error, scope: "openid read:me:factors read:me:authentication_methods") { [weak self] in Task { await self?.loadMyAccountAuthViewComponentData() diff --git a/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift b/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift index fbf331c..ce1d11b 100644 --- a/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift +++ b/Auth0UniversalComponents/Passkeys/Presentation/PasskeysEnrollmentViewModel.swift @@ -129,24 +129,32 @@ final class PasskeysEnrollmentViewModel: NSObject, func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: any Error) { showLoader = false - if let authError = error as? ASAuthorizationError, authError.code != .canceled { - errorViewModel = Auth0UIComponentError.unknown().errorViewModel { [weak self] in - Task { - await self?.startEnrollment() - } - } - } else { + guard let authError = error as? ASAuthorizationError else { Task { [weak self] in await self?.handle( error: error, scope: "openid create:me:authentication_methods", retryCallback: { - Task { - await self?.startEnrollment() - } + Task { await self?.startEnrollment() } } ) } + return + } + + switch authError.code { + case .canceled: + break + case .failed: + // Code 1004: platform rejected the request — most commonly a domain association + // misconfiguration (entitlements webcredentials entry or AASA file on the server). + errorViewModel = Auth0UIComponentError.unknown(message: authError.localizedDescription).errorViewModel { [weak self] in + Task { await self?.startEnrollment() } + } + default: + errorViewModel = Auth0UIComponentError.unknown().errorViewModel { [weak self] in + Task { await self?.startEnrollment() } + } } }