From f4018bd7085b471556529c01aac65f68943ced0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:01:36 +0530 Subject: [PATCH 01/10] build(deps): bump codecov/codecov-action from 5.5.3 to 6.0.0 (#794) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fba30df0..5a0c6af6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -451,28 +451,28 @@ jobs: path: coverage/android - name: Upload coverage report for auth0_flutter - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 with: name: Auth0 Flutter flags: auth0_flutter directory: coverage/auth0_flutter - name: Upload coverage report for auth0_flutter_platform_interface - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 with: name: Auth0 Flutter flags: auth0_flutter_platform_interface directory: coverage/auth0_flutter_platform_interface - name: Upload coverage report for iOS - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 with: name: Auth0 Flutter flags: auth0_flutter_ios directory: coverage/ios - name: Upload coverage report for Android - uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 with: name: Auth0 Flutter flags: auth0_flutter_android From b67744c951139dbe0feaddbdbe46989bc8434dd4 Mon Sep 17 00:00:00 2001 From: Utkrisht Sahu Date: Tue, 31 Mar 2026 13:43:45 +0530 Subject: [PATCH 02/10] feat: Expose isRetryable property on CredentialsManagerException, ApiException, and WebAuthenticationException (#786) --- auth0_flutter/EXAMPLES.md | 37 +++++++- .../CredentialsManagerExceptionExtensions.kt | 19 ++++ .../GetCredentialsRequestHandler.kt | 2 +- .../GetSSOCredentialsRequestHandler.kt | 3 +- .../RenewCredentialsRequestHandler.kt | 2 +- .../web_auth/LoginWebAuthRequestHandler.kt | 7 +- .../web_auth/LogoutWebAuthRequestHandler.kt | 7 +- ...edentialsManagerExceptionExtensionsTest.kt | 94 +++++++++++++++++++ .../LoginWebAuthRequestHandlerTest.kt | 31 +++++- .../LogoutWebAuthRequestHandlerTest.kt | 31 +++++- .../CredentialsManagerExtensions.swift | 7 +- .../Classes/WebAuth/WebAuthExtensions.swift | 5 +- auth0_flutter/darwin/auth0_flutter.podspec | 2 +- .../CredentialsManagerExtensionsTests.swift | 20 ++++ .../example/ios/Tests/Utilities.swift | 12 +++ .../WebAuth/WebAuthExtensionsTests.swift | 20 ++++ auth0_flutter/ios/auth0_flutter.podspec | 2 +- auth0_flutter/macos/auth0_flutter.podspec | 2 +- .../lib/src/auth/api_exception.dart | 1 + .../credentials_manager_exception.dart | 7 ++ .../web_authentication_exception.dart | 3 + .../test/api_exception_test.dart | 31 ++++++ .../credential_manager_exception_test.dart | 33 +++++++ .../web_authentication_exception_test.dart | 33 +++++++ 24 files changed, 397 insertions(+), 14 deletions(-) create mode 100644 auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensions.kt create mode 100644 auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensionsTest.kt diff --git a/auth0_flutter/EXAMPLES.md b/auth0_flutter/EXAMPLES.md index b85355e5..e8210234 100644 --- a/auth0_flutter/EXAMPLES.md +++ b/auth0_flutter/EXAMPLES.md @@ -345,10 +345,16 @@ try { final credentials = await auth0.webAuthentication().login(); // ... } on WebAuthenticationException catch (e) { - print(e); + if (e.isRetryable) { + // Transient error (e.g. network issue) — safe to retry + } else { + print(e); + } } ``` +The `isRetryable` property indicates whether the error is transient (e.g. a network outage) and the operation can be retried. +
@@ -521,6 +527,27 @@ print(e); } ``` +#### Retryable errors + +The `isRetryable` property on `CredentialsManagerException` indicates whether the error is transient and the operation can be retried. When `true`, the failure is likely due to a temporary condition such as a network outage. When `false`, the failure is permanent (e.g. an invalid refresh token) and retrying will not help — you should log the user out instead. + +```dart +try { + final credentials = await auth0.credentialsManager.credentials(); + // ... +} on CredentialsManagerException catch (e) { + if (e.isRetryable) { + // Transient error (e.g. network issue) — safe to retry + print("Temporary error, retrying..."); + } else { + // Permanent error — log the user out + print("Credentials cannot be renewed: ${e.message}"); + } +} +``` + +> The `isRetryable` property is available on all exception types (`CredentialsManagerException`, `ApiException`, `WebAuthenticationException`) across Android and iOS/macOS. It returns `true` when the underlying failure is network-related, indicating the operation may succeed on retry. + ### Native to Web SSO Native to Web SSO allows authenticated users in your native mobile application to seamlessly transition to your web application without requiring them to log in again. This is achieved by exchanging a refresh token for a Session Transfer Token, which can then be used to establish a session in the web application. @@ -891,10 +918,16 @@ try { connectionOrRealm: connection); // ... } on ApiException catch (e) { - print(e); + if (e.isRetryable) { + // Transient error (e.g. network issue) — safe to retry + } else { + print(e); + } } ``` +The `isRetryable` property indicates whether the error is transient (e.g. a network outage) and the operation can be retried. It returns `true` when `isNetworkError` is `true`. + [Go up ⤴](#examples) ## 🌐📱 Organizations diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensions.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensions.kt new file mode 100644 index 00000000..1cfa8688 --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensions.kt @@ -0,0 +1,19 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.CredentialsManagerException + +fun CredentialsManagerException.toMap(): Map { + val exceptionCause = this.cause + val isRetryable = when (exceptionCause) { + is AuthenticationException -> exceptionCause.isNetworkError + else -> false + } + + val map = mutableMapOf("_isRetryable" to isRetryable) + if (exceptionCause != null) { + map["cause"] = exceptionCause.toString() + map["causeStackTrace"] = exceptionCause.stackTraceToString() + } + return map +} diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt index 664d5ea2..45abd90a 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetCredentialsRequestHandler.kt @@ -32,7 +32,7 @@ class GetCredentialsRequestHandler : CredentialsManagerRequestHandler { credentialsManager.getCredentials(scope, minTtl, parameters, object: Callback { override fun onFailure(exception: CredentialsManagerException) { - result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception) + result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap()) } override fun onSuccess(credentials: Credentials) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt index 7e2fd73f..25aed859 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/GetSSOCredentialsRequestHandler.kt @@ -6,6 +6,7 @@ import com.auth0.android.authentication.storage.SecureCredentialsManager import com.auth0.android.callback.Callback import com.auth0.android.result.SSOCredentials import com.auth0.auth0_flutter.request_handlers.MethodCallRequest +import com.auth0.auth0_flutter.toMap import io.flutter.plugin.common.MethodChannel class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler { @@ -22,7 +23,7 @@ class GetSSOCredentialsRequestHandler : CredentialsManagerRequestHandler { credentialsManager.getSsoCredentials(parameters, object : Callback { override fun onFailure(exception: CredentialsManagerException) { - result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception) + result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap()) } override fun onSuccess(credentials: SSOCredentials) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt index 7124511d..a4f1c4db 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/credentials_manager/RenewCredentialsRequestHandler.kt @@ -24,7 +24,7 @@ class RenewCredentialsRequestHandler : CredentialsManagerRequestHandler { credentialsManager.getCredentials(null, 0, parameters, true, object : Callback { override fun onFailure(exception: CredentialsManagerException) { - result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception) + result.error(exception.message ?: "UNKNOWN ERROR", exception.message, exception.toMap()) } override fun onSuccess(credentials: Credentials) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt index e21e983e..eb8f764b 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt @@ -80,7 +80,12 @@ class LoginWebAuthRequestHandler( builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), exception) + val details = mutableMapOf("_isRetryable" to exception.isNetworkError) + exception.cause?.let { + details["cause"] = it.toString() + details["causeStackTrace"] = it.stackTraceToString() + } + result.error(exception.getCode(), exception.getDescription(), details) } override fun onSuccess(credentials: Credentials) { diff --git a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt index 3e8f90a6..a120b1ff 100644 --- a/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LogoutWebAuthRequestHandler.kt @@ -46,7 +46,12 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), exception) + val details = mutableMapOf("_isRetryable" to exception.isNetworkError) + exception.cause?.let { + details["cause"] = it.toString() + details["causeStackTrace"] = it.stackTraceToString() + } + result.error(exception.getCode(), exception.getDescription(), details) } override fun onSuccess(res: Void?) { diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensionsTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensionsTest.kt new file mode 100644 index 00000000..94a3c6ea --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensionsTest.kt @@ -0,0 +1,94 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.CredentialsManagerException +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.`when` +import org.mockito.Mockito.mock +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CredentialsManagerExceptionExtensionsTest { + + @Test + fun `should set isRetryable to false when cause is null`() { + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(null) + + val map = exception.toMap() + + assertThat(map["_isRetryable"], equalTo(false)) + } + + @Test + fun `should set isRetryable to true when cause is a network AuthenticationException`() { + val authException = mock(AuthenticationException::class.java) + `when`(authException.isNetworkError).thenReturn(true) + + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(authException) + + val map = exception.toMap() + + assertThat(map["_isRetryable"], equalTo(true)) + } + + @Test + fun `should set isRetryable to false when cause is a non-network AuthenticationException`() { + val authException = mock(AuthenticationException::class.java) + `when`(authException.isNetworkError).thenReturn(false) + + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(authException) + + val map = exception.toMap() + + assertThat(map["_isRetryable"], equalTo(false)) + } + + @Test + fun `should set isRetryable to false when cause is a generic exception`() { + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(RuntimeException("generic error")) + + val map = exception.toMap() + + assertThat(map["_isRetryable"], equalTo(false)) + } + + @Test + fun `should include cause in map when cause is present`() { + val cause = RuntimeException("network error") + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(cause) + + val map = exception.toMap() + + assertThat(map["cause"], equalTo(cause.toString())) + } + + @Test + fun `should include causeStackTrace in map when cause is present`() { + val cause = RuntimeException("network error") + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(cause) + + val map = exception.toMap() + + assertThat(map["causeStackTrace"], equalTo(cause.stackTraceToString())) + } + + @Test + fun `should not include cause or causeStackTrace in map when cause is null`() { + val exception = mock(CredentialsManagerException::class.java) + `when`(exception.cause).thenReturn(null) + + val map = exception.toMap() + + assertThat(map.containsKey("cause"), equalTo(false)) + assertThat(map.containsKey("causeStackTrace"), equalTo(false)) + } +} diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt index f24d6753..6db3c70c 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LoginWebAuthRequestHandlerTest.kt @@ -305,7 +305,36 @@ class LoginWebAuthRequestHandlerTest { val mockRequest = MethodCallRequest(mockAccount, hashMapOf()) handler.handle(mock(), mockRequest, mockResult) - verify(mockResult).error("code", "description", exception) + verify(mockResult).error(eq("code"), eq("description"), eq(mapOf("_isRetryable" to false))) + } + + @Test + fun `returns cause and causeStackTrace in error details when cause is present`() { + val builder = mock() + val mockResult = mock() + val cause = RuntimeException("network error") + val exception = mock() + whenever(exception.getCode()).thenReturn("code") + whenever(exception.getDescription()).thenReturn("description") + whenever(exception.isNetworkError).thenReturn(true) + whenever(exception.cause).thenReturn(cause) + + doAnswer { invocation -> + val cb = invocation.getArgument>(1) + cb.onFailure(exception) + }.`when`(builder).start(any(), any()) + + val handler = LoginWebAuthRequestHandler { _ -> builder } + val mockAccount = mock() + val mockRequest = MethodCallRequest(mockAccount, hashMapOf()) + handler.handle(mock(), mockRequest, mockResult) + + verify(mockResult).error(eq("code"), eq("description"), check { + val map = it as Map<*, *> + assertThat(map["_isRetryable"], equalTo(true)) + assertThat(map["cause"], equalTo(cause.toString())) + assertThat(map["causeStackTrace"], equalTo(cause.stackTraceToString())) + }) } @Test diff --git a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt index e0efb40a..5666f491 100644 --- a/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/LogoutWebAuthRequestHandlerTest.kt @@ -9,6 +9,8 @@ import com.auth0.android.provider.WebAuthProvider import com.auth0.auth0_flutter.request_handlers.MethodCallRequest import com.auth0.auth0_flutter.request_handlers.web_auth.LogoutWebAuthRequestHandler import io.flutter.plugin.common.MethodChannel.Result +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.* @@ -122,7 +124,34 @@ class LogoutWebAuthRequestHandlerTest { handler.handle(mock(), MethodCallRequest(Auth0.getInstance("test-client", "test-domain"), mock()), mockResult) - verify(mockResult).error("code", "description", exception) + verify(mockResult).error(eq("code"), eq("description"), eq(mapOf("_isRetryable" to false))) + } + + @Test + fun `returns cause and causeStackTrace in error details when cause is present`() { + val mockBuilder = mock() + val mockResult = mock() + val handler = LogoutWebAuthRequestHandler { mockBuilder } + val cause = RuntimeException("network error") + val exception = mock() + whenever(exception.getCode()).thenReturn("code") + whenever(exception.getDescription()).thenReturn("description") + whenever(exception.isNetworkError).thenReturn(true) + whenever(exception.cause).thenReturn(cause) + + doAnswer { invocation -> + val callback = invocation.getArgument>(1) + callback.onFailure(exception) + }.`when`(mockBuilder).start(any(), any()) + + handler.handle(mock(), MethodCallRequest(Auth0.getInstance("test-client", "test-domain"), mock()), mockResult) + + verify(mockResult).error(eq("code"), eq("description"), check { + val map = it as Map<*, *> + assertThat(map["_isRetryable"], equalTo(true)) + assertThat(map["cause"], equalTo(cause.toString())) + assertThat(map["causeStackTrace"], equalTo(cause.stackTraceToString())) + }) } @Test diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift index 9401dffe..a3efc75b 100644 --- a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift @@ -20,8 +20,13 @@ extension FlutterError { default: code = "UNKNOWN" } + let isRetryable = (credentialsManagerError.cause as? Auth0APIError)?.isRetryable ?? false + + var errorDetails = credentialsManagerError.details + errorDetails["_isRetryable"] = isRetryable + self.init(code: code, message: String(describing: credentialsManagerError), - details: credentialsManagerError.details) + details: errorDetails) } } diff --git a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift index fe7318a9..263c728e 100644 --- a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift @@ -20,7 +20,10 @@ extension FlutterError { case .other: code = "OTHER" default: code = "UNKNOWN" } - self.init(code: code, message: String(describing: webAuthError), details: webAuthError.details) + var details = webAuthError.details + let isRetryable = (webAuthError.cause as? Auth0APIError)?.isRetryable ?? false + details["_isRetryable"] = isRetryable + self.init(code: code, message: String(describing: webAuthError), details: details) } } diff --git a/auth0_flutter/darwin/auth0_flutter.podspec b/auth0_flutter/darwin/auth0_flutter.podspec index 5797d129..83078992 100644 --- a/auth0_flutter/darwin/auth0_flutter.podspec +++ b/auth0_flutter/darwin/auth0_flutter.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.16.2' + s.dependency 'Auth0', '2.18.0' s.dependency 'JWTDecode', '3.3.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index c9fa1e60..3af6ade4 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -19,4 +19,24 @@ class CredentialsManagerExtensionsTests: XCTestCase { assert(flutterError: flutterError, is: error, with: code) } } + + func testIsRetryableIsFalseForErrorsWithoutNetworkCause() { + let nonRetryableErrors: [CredentialsManagerError] = [ + .noCredentials, + .noRefreshToken, + .renewFailed, + .storeFailed, + .biometricsFailed, + .revokeFailed, + .largeMinTTL + ] + for error in nonRetryableErrors { + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertEqual(details["_isRetryable"] as? Bool, false, + "Expected isRetryable to be false for \(error)") + } + } + + } diff --git a/auth0_flutter/example/ios/Tests/Utilities.swift b/auth0_flutter/example/ios/Tests/Utilities.swift index 74963e74..2828969c 100644 --- a/auth0_flutter/example/ios/Tests/Utilities.swift +++ b/auth0_flutter/example/ios/Tests/Utilities.swift @@ -112,6 +112,12 @@ func assert(result: Any?, isError handlerError: HandlerError) { func assert(flutterError: FlutterError, is webAuthError: WebAuthError, with code: String) { XCTAssertEqual(flutterError.code, code) XCTAssertEqual(flutterError.message, String(describing: webAuthError)) + + guard let details = flutterError.details as? [String: Any] else { + return XCTFail("The FlutterError is missing the 'details' dictionary") + } + XCTAssertNotNil(details["_isRetryable"]) + XCTAssertTrue(details["_isRetryable"] is Bool) } func assert(flutterError: FlutterError, is authenticationError: AuthenticationError) { @@ -151,4 +157,10 @@ func assert(flutterError: FlutterError, is authenticationError: AuthenticationEr func assert(flutterError: FlutterError, is credentialsManagerError: CredentialsManagerError, with code: String) { XCTAssertEqual(flutterError.code, code) XCTAssertEqual(flutterError.message, String(describing: credentialsManagerError)) + + guard let details = flutterError.details as? [String: Any] else { + return XCTFail("The FlutterError is missing the 'details' dictionary") + } + XCTAssertNotNil(details["_isRetryable"]) + XCTAssertTrue(details["_isRetryable"] is Bool) } diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index 1b8fe238..4bef3c43 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift @@ -21,4 +21,24 @@ class WebAuthExtensionsTests: XCTestCase { assert(flutterError: flutterError, is: error, with: code) } } + + func testIsRetryableIsFalseForNonNetworkErrors() { + let nonRetryableErrors: [WebAuthError] = [ + .userCancelled, + .noBundleIdentifier, + .invalidInvitationURL, + .noAuthorizationCode, + .pkceNotAllowed, + .idTokenValidationFailed, + .transactionActiveAlready, + .other + ] + for error in nonRetryableErrors { + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertEqual(details["_isRetryable"] as? Bool, false, + "Expected isRetryable to be false for \(error)") + } + } + } diff --git a/auth0_flutter/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index 5797d129..83078992 100644 --- a/auth0_flutter/ios/auth0_flutter.podspec +++ b/auth0_flutter/ios/auth0_flutter.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.16.2' + s.dependency 'Auth0', '2.18.0' s.dependency 'JWTDecode', '3.3.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter/macos/auth0_flutter.podspec b/auth0_flutter/macos/auth0_flutter.podspec index 5797d129..83078992 100644 --- a/auth0_flutter/macos/auth0_flutter.podspec +++ b/auth0_flutter/macos/auth0_flutter.podspec @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.osx.deployment_target = '11.0' s.osx.dependency 'FlutterMacOS' - s.dependency 'Auth0', '2.16.2' + s.dependency 'Auth0', '2.18.0' s.dependency 'JWTDecode', '3.3.0' s.dependency 'SimpleKeychain', '1.3.0' diff --git a/auth0_flutter_platform_interface/lib/src/auth/api_exception.dart b/auth0_flutter_platform_interface/lib/src/auth/api_exception.dart index bf03dcbd..02cd441d 100644 --- a/auth0_flutter_platform_interface/lib/src/auth/api_exception.dart +++ b/auth0_flutter_platform_interface/lib/src/auth/api_exception.dart @@ -72,4 +72,5 @@ class ApiException extends Auth0Exception { bool get isPasswordLeaked => _errorFlags.getBooleanOrFalse('isPasswordLeaked'); bool get isLoginRequired => _errorFlags.getBooleanOrFalse('isLoginRequired'); + bool get isRetryable => isNetworkError; } diff --git a/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart index ec8ffdcc..4f463c47 100644 --- a/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart +++ b/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart @@ -2,6 +2,7 @@ import 'package:flutter/services.dart'; import '../auth0_exception.dart'; import '../extensions/exception_extensions.dart'; +import '../extensions/map_extensions.dart'; // ignore: comment_references /// Exception thrown by [MethodChannelCredentialsManager] when something goes @@ -37,4 +38,10 @@ An error occurred while trying to use the Refresh Token to renew the Credentials code == ''' Credentials need to be renewed but no Refresh Token is available to renew them.'''; + + /// Whether the error is transient and the operation can be retried. + /// When `true`, the failure is likely due to a temporary condition such as + /// a network outage. When `false`, the failure is permanent (e.g. an + /// invalid refresh token) and the user should be logged out instead. + bool get isRetryable => details.getBooleanOrFalse('_isRetryable'); } diff --git a/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart b/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart index dcfa87c5..8e3d0b40 100644 --- a/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart +++ b/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart @@ -2,6 +2,7 @@ import 'package:flutter/services.dart'; import '../auth0_exception.dart'; import '../extensions/exception_extensions.dart'; +import '../extensions/map_extensions.dart'; class WebAuthenticationException extends Auth0Exception { const WebAuthenticationException(final String code, final String message, @@ -16,4 +17,6 @@ class WebAuthenticationException extends Auth0Exception { bool get isUserCancelledException => code == 'USER_CANCELLED' || code == 'a0.authentication_canceled'; + + bool get isRetryable => details.getBooleanOrFalse('_isRetryable'); } diff --git a/auth0_flutter_platform_interface/test/api_exception_test.dart b/auth0_flutter_platform_interface/test/api_exception_test.dart index 7793de30..3cedc27f 100644 --- a/auth0_flutter_platform_interface/test/api_exception_test.dart +++ b/auth0_flutter_platform_interface/test/api_exception_test.dart @@ -251,4 +251,35 @@ void main() { final exception = ApiException.fromPlatformException(platformException); expect(exception.mfaToken, null); }); + + test('isRetryable returns true when isNetworkError is true', () async { + final details = { + '_errorFlags': {'isNetworkError': true} + }; + final platformException = + PlatformException(code: 'test-code', details: details); + + final exception = ApiException.fromPlatformException(platformException); + expect(exception.isRetryable, true); + }); + + test('isRetryable returns false when isNetworkError is false', () async { + final details = { + '_errorFlags': {'isNetworkError': false} + }; + final platformException = + PlatformException(code: 'test-code', details: details); + + final exception = ApiException.fromPlatformException(platformException); + expect(exception.isRetryable, false); + }); + + test('isRetryable returns false when errorFlags are missing', () async { + final details = {}; + final platformException = + PlatformException(code: 'test-code', details: details); + + final exception = ApiException.fromPlatformException(platformException); + expect(exception.isRetryable, false); + }); } diff --git a/auth0_flutter_platform_interface/test/credential_manager_exception_test.dart b/auth0_flutter_platform_interface/test/credential_manager_exception_test.dart index 4c7f0e97..1b0d4a74 100644 --- a/auth0_flutter_platform_interface/test/credential_manager_exception_test.dart +++ b/auth0_flutter_platform_interface/test/credential_manager_exception_test.dart @@ -18,5 +18,38 @@ void main() { expect(exception.message, 'test-message'); expect(exception.details['details-prop'], 'details-value'); }); + + test('isRetryable returns true when _isRetryable flag is true', () { + final details = {'_isRetryable': true}; + final platformException = PlatformException( + code: 'RENEW_FAILED', message: 'test-message', details: details); + + final exception = + CredentialsManagerException.fromPlatformException(platformException); + + expect(exception.isRetryable, true); + }); + + test('isRetryable returns false when _isRetryable flag is false', () { + final details = {'_isRetryable': false}; + final platformException = PlatformException( + code: 'RENEW_FAILED', message: 'test-message', details: details); + + final exception = + CredentialsManagerException.fromPlatformException(platformException); + + expect(exception.isRetryable, false); + }); + + test('isRetryable returns false when _isRetryable flag is missing', () { + final details = {}; + final platformException = PlatformException( + code: 'RENEW_FAILED', message: 'test-message', details: details); + + final exception = + CredentialsManagerException.fromPlatformException(platformException); + + expect(exception.isRetryable, false); + }); }); } diff --git a/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart b/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart index 42eee995..d81fb797 100644 --- a/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart +++ b/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart @@ -18,5 +18,38 @@ void main() { expect(exception.message, 'test-message'); expect(exception.details['details-prop'], 'details-value'); }); + + test('isRetryable returns true when _isRetryable flag is true', () { + final details = {'_isRetryable': true}; + final platformException = PlatformException( + code: 'OTHER', message: 'test-message', details: details); + + final exception = + WebAuthenticationException.fromPlatformException(platformException); + + expect(exception.isRetryable, true); + }); + + test('isRetryable returns false when _isRetryable flag is false', () { + final details = {'_isRetryable': false}; + final platformException = PlatformException( + code: 'USER_CANCELLED', message: 'test-message', details: details); + + final exception = + WebAuthenticationException.fromPlatformException(platformException); + + expect(exception.isRetryable, false); + }); + + test('isRetryable returns false when _isRetryable flag is missing', () { + final details = {}; + final platformException = PlatformException( + code: 'OTHER', message: 'test-message', details: details); + + final exception = + WebAuthenticationException.fromPlatformException(platformException); + + expect(exception.isRetryable, false); + }); }); } From 18acf0cee6cd3e27b1482de59e69e6ee6976848b Mon Sep 17 00:00:00 2001 From: Utkrisht Sahu Date: Thu, 2 Apr 2026 10:36:05 +0530 Subject: [PATCH 03/10] chore: add SCA scan workflow (#803) --- .github/workflows/sca_scan.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/sca_scan.yml diff --git a/.github/workflows/sca_scan.yml b/.github/workflows/sca_scan.yml new file mode 100644 index 00000000..a945d5a4 --- /dev/null +++ b/.github/workflows/sca_scan.yml @@ -0,0 +1,13 @@ +name: SCA + +on: + pull_request: + branches: ["main"] + workflow_dispatch: + +jobs: + snyk-cli: + uses: auth0/devsecops-tooling/.github/workflows/sca-scan.yml@main + with: + java-version: "17" + secrets: inherit From ea0ee81cb2bda5fade92544374dcb1438fa9c405 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:34:16 +0530 Subject: [PATCH 04/10] build(deps): bump ruby/setup-ruby from 1.299.0 to 1.300.0 in /.github/actions/setup-darwin (#806) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/setup-darwin/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-darwin/action.yml b/.github/actions/setup-darwin/action.yml index a49b2ad4..1e612c5b 100644 --- a/.github/actions/setup-darwin/action.yml +++ b/.github/actions/setup-darwin/action.yml @@ -36,7 +36,7 @@ runs: shell: bash - name: Set up Ruby - uses: ruby/setup-ruby@3ff19f5e2baf30647122352b96108b1fbe250c64 # pin@v1.299.0 + uses: ruby/setup-ruby@e65c17d16e57e481586a6a5a0282698790062f92 # pin@v1.300.0 with: ruby-version: ${{ inputs.ruby }} bundler-cache: true From db639847c6d993cf9243c6d905192ca261b37e49 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:24:06 +0530 Subject: [PATCH 05/10] build(deps): bump aws-actions/configure-aws-credentials from 6.0.0 to 6.1.0 in /.github/actions/rl-scanner (#807) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/rl-scanner/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/rl-scanner/action.yml b/.github/actions/rl-scanner/action.yml index f5a16441..1b424cdf 100644 --- a/.github/actions/rl-scanner/action.yml +++ b/.github/actions/rl-scanner/action.yml @@ -23,7 +23,7 @@ runs: pip install boto3 requests - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # pin@v4.3.0 + uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # pin@v4.3.0 with: role-to-assume: ${{ env.PRODSEC_TOOLS_ARN }} aws-region: 'us-east-1' From 4f5399363ca52ff4f53359bcf80329a5efcdf1e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:26:49 +0530 Subject: [PATCH 06/10] build(deps-dev): bump basic-ftp from 5.2.0 to 5.2.1 in /appium-test (#809) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- appium-test/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appium-test/package-lock.json b/appium-test/package-lock.json index e2b6f96b..2230fa44 100644 --- a/appium-test/package-lock.json +++ b/appium-test/package-lock.json @@ -461,9 +461,9 @@ "license": "MIT" }, "node_modules/basic-ftp": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", - "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.1.tgz", + "integrity": "sha512-0yaL8JdxTknKDILitVpfYfV2Ob6yb3udX/hK97M7I3jOeznBNxQPtVvTUtnhUkyHlxFWyr5Lvknmgzoc7jf+1Q==", "dev": true, "license": "MIT", "engines": { From 0587946efbd891827ecb6ef7e25fd6dce7d29415 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:28:46 +0530 Subject: [PATCH 07/10] build(deps-dev): bump lodash from 4.17.23 to 4.18.1 in /appium-test (#805) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> --- appium-test/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appium-test/package-lock.json b/appium-test/package-lock.json index 2230fa44..c9dcb44c 100644 --- a/appium-test/package-lock.json +++ b/appium-test/package-lock.json @@ -1807,9 +1807,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, From 878add681b637c8369e0915ded123c06b97adbd0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:44:02 +0530 Subject: [PATCH 08/10] build(deps): bump subosito/flutter-action from 2.22.0 to 2.23.0 in /.github/actions/setup-publish (#792) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> --- .github/actions/setup-publish/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/setup-publish/action.yml b/.github/actions/setup-publish/action.yml index d3e1a104..f6e44d24 100644 --- a/.github/actions/setup-publish/action.yml +++ b/.github/actions/setup-publish/action.yml @@ -15,7 +15,7 @@ runs: steps: - name: Install Flutter - uses: subosito/flutter-action@0ca7a949e71ae44c8e688a51c5e7e93b2c87e295 # pin@v2.22.0 + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # pin@v2.23.0 with: flutter-version: ${{ inputs.flutter }} channel: stable From 0db341fa70828d32bc69ec88e10f439f73feadbd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:55:52 +0530 Subject: [PATCH 09/10] build(deps): bump activesupport from 7.0.7.2 to 7.2.3.1 in /auth0_flutter/example/ios (#788) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> --- auth0_flutter/example/ios/Gemfile.lock | 28 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/auth0_flutter/example/ios/Gemfile.lock b/auth0_flutter/example/ios/Gemfile.lock index 28fd29b6..bc587521 100644 --- a/auth0_flutter/example/ios/Gemfile.lock +++ b/auth0_flutter/example/ios/Gemfile.lock @@ -3,20 +3,33 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.0.7.2) - concurrent-ruby (~> 1.0, >= 1.0.2) + activesupport (7.2.3.1) + base64 + benchmark (>= 0.3) + bigdecimal + concurrent-ruby (~> 1.0, >= 1.3.1) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) - minitest (>= 5.1) - tzinfo (~> 2.0) + logger (>= 1.4.2) + minitest (>= 5.1, < 6) + securerandom (>= 0.3) + tzinfo (~> 2.0, >= 2.0.5) atomos (0.1.3) + base64 (0.3.0) + benchmark (0.5.0) + bigdecimal (4.0.1) claide (1.1.0) clamp (1.3.2) colored2 (3.1.2) - concurrent-ruby (1.2.2) - i18n (1.14.1) + concurrent-ruby (1.3.6) + connection_pool (3.0.2) + drb (2.2.3) + i18n (1.14.8) concurrent-ruby (~> 1.0) + logger (1.7.0) mini_portile2 (2.8.9) - minitest (5.19.0) + minitest (5.27.0) nanaimo (0.3.0) nokogiri (1.19.1) mini_portile2 (~> 2.8.2) @@ -25,6 +38,7 @@ GEM racc (~> 1.4) racc (1.8.1) rexml (3.4.2) + securerandom (0.4.1) slather (2.7.4) CFPropertyList (>= 2.2, < 4) activesupport From e3a5e230bb095b35e887b493d0bebd3618847ce8 Mon Sep 17 00:00:00 2001 From: Artem Zelinskyi <151676331+crazycatk@users.noreply.github.com> Date: Sun, 12 Apr 2026 05:44:16 +0200 Subject: [PATCH 10/10] Set explicit type for .other in switch in FlutterError (#808) Co-authored-by: sanchitmehtagit Co-authored-by: Prince Mathew <17837162+pmathew92@users.noreply.github.com> --- .../Classes/WebAuth/WebAuthExtensions.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift index 263c728e..6f706b99 100644 --- a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift @@ -10,14 +10,14 @@ extension FlutterError { convenience init(from webAuthError: WebAuthError) { var code: String switch webAuthError { - case .noBundleIdentifier: code = "NO_BUNDLE_IDENTIFIER" - case .invalidInvitationURL: code = "INVALID_INVITATION_URL" - case .userCancelled: code = "USER_CANCELLED" - case .noAuthorizationCode: code = "NO_AUTHORIZATION_CODE" - case .pkceNotAllowed: code = "PKCE_NOT_ALLOWED" - case .idTokenValidationFailed: code = "ID_TOKEN_VALIDATION_FAILED" - case .transactionActiveAlready: code = "TRANSACTION_ACTIVE_ALREADY" - case .other: code = "OTHER" + case WebAuthError.noBundleIdentifier: code = "NO_BUNDLE_IDENTIFIER" + case WebAuthError.invalidInvitationURL: code = "INVALID_INVITATION_URL" + case WebAuthError.userCancelled: code = "USER_CANCELLED" + case WebAuthError.noAuthorizationCode: code = "NO_AUTHORIZATION_CODE" + case WebAuthError.pkceNotAllowed: code = "PKCE_NOT_ALLOWED" + case WebAuthError.idTokenValidationFailed: code = "ID_TOKEN_VALIDATION_FAILED" + case WebAuthError.transactionActiveAlready: code = "TRANSACTION_ACTIVE_ALREADY" + case WebAuthError.other: code = "OTHER" default: code = "UNKNOWN" } var details = webAuthError.details