From 36277ddfc205f3368297b2d4b9c172decae7a99f Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Mar 2026 23:02:16 +0530 Subject: [PATCH 1/9] feat: add isRetryable property to CredentialsManagerException --- auth0_flutter/EXAMPLES.md | 21 +++++++ .../CredentialsManagerExceptionExtensions.kt | 13 ++++ .../GetCredentialsRequestHandler.kt | 2 +- .../GetSSOCredentialsRequestHandler.kt | 3 +- .../RenewCredentialsRequestHandler.kt | 2 +- ...edentialsManagerExceptionExtensionsTest.kt | 61 +++++++++++++++++++ .../CredentialsManagerExtensions.swift | 19 +++++- .../CredentialsManagerExtensionsTests.swift | 32 ++++++++++ .../example/ios/Tests/Utilities.swift | 6 ++ .../credentials_manager_exception.dart | 7 +++ .../credential_manager_exception_test.dart | 33 ++++++++++ 11 files changed, 195 insertions(+), 4 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 b85355e5e..6642ccb90 100644 --- a/auth0_flutter/EXAMPLES.md +++ b/auth0_flutter/EXAMPLES.md @@ -521,6 +521,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}"); + } +} +``` + +> This property is available on Android and iOS/macOS. On Android, it checks whether the underlying cause is a network-related `AuthenticationException`. On iOS/macOS, it checks for `AuthenticationError.isNetworkError` or `URLError`, and also returns `true` for biometrics failures. + ### 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. 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 000000000..259cea30c --- /dev/null +++ b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensions.kt @@ -0,0 +1,13 @@ +package com.auth0.auth0_flutter + +import com.auth0.android.authentication.AuthenticationException +import com.auth0.android.authentication.storage.CredentialsManagerException + +fun CredentialsManagerException.toMap(): Map { + val isRetryable = when (val exceptionCause = this.cause) { + is AuthenticationException -> exceptionCause.isNetworkError + else -> false + } + + return mapOf("_isRetryable" to isRetryable) +} 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 664d5ea2f..45abd90ae 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 7e2fd73ff..25aed8597 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 7124511d3..a4f1c4db0 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/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 000000000..af92e8b22 --- /dev/null +++ b/auth0_flutter/android/src/test/kotlin/com/auth0/auth0_flutter/CredentialsManagerExceptionExtensionsTest.kt @@ -0,0 +1,61 @@ +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)) + } +} diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift index 9401dffe0..bf9f39865 100644 --- a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift @@ -20,8 +20,25 @@ extension FlutterError { default: code = "UNKNOWN" } + let isRetryable: Bool + switch credentialsManagerError { + case .renewFailed: + if let authError = credentialsManagerError.cause as? AuthenticationError { + isRetryable = authError.isNetworkError + } else { + isRetryable = credentialsManagerError.cause is URLError + } + case .biometricsFailed: + isRetryable = true + default: + 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/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index c9fa1e602..1a50190a3 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -19,4 +19,36 @@ class CredentialsManagerExtensionsTests: XCTestCase { assert(flutterError: flutterError, is: error, with: code) } } + + func testIsRetryableIsFalseForNonRetryableErrors() { + let nonRetryableErrors: [CredentialsManagerError] = [ + .noCredentials, + .noRefreshToken, + .renewFailed, + .storeFailed, + .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)") + } + } + + func testIsRetryableIsTrueForBiometricsFailed() { + let flutterError = FlutterError(from: .biometricsFailed) + let details = flutterError.details as! [String: Any] + XCTAssertEqual(details["_isRetryable"] as? Bool, true) + } + + func testIsRetryableIsTrueForRenewFailedWithNetworkError() { + let networkError = AuthenticationError(info: [:], statusCode: 0) + let renewError = CredentialsManagerError(code: .renewFailed, cause: networkError) + let flutterError = FlutterError(from: renewError) + let details = flutterError.details as! [String: Any] + // AuthenticationError with statusCode 0 and empty info is treated as a network error + XCTAssertNotNil(details["_isRetryable"]) + } } diff --git a/auth0_flutter/example/ios/Tests/Utilities.swift b/auth0_flutter/example/ios/Tests/Utilities.swift index 74963e744..51e0a2f6f 100644 --- a/auth0_flutter/example/ios/Tests/Utilities.swift +++ b/auth0_flutter/example/ios/Tests/Utilities.swift @@ -151,4 +151,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_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart b/auth0_flutter_platform_interface/lib/src/credentials-manager/credentials_manager_exception.dart index ec8ffdccc..4f463c47c 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/test/credential_manager_exception_test.dart b/auth0_flutter_platform_interface/test/credential_manager_exception_test.dart index 4c7f0e97b..1b0d4a741 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); + }); }); } From 34d7da82ac85d11d6fcc61436cd6096a4b8644c3 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 23 Mar 2026 23:20:15 +0530 Subject: [PATCH 2/9] resolving ios UT failure --- .../CredentialsManagerExtensionsTests.swift | 8 -------- 1 file changed, 8 deletions(-) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index 1a50190a3..571916c31 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -43,12 +43,4 @@ class CredentialsManagerExtensionsTests: XCTestCase { XCTAssertEqual(details["_isRetryable"] as? Bool, true) } - func testIsRetryableIsTrueForRenewFailedWithNetworkError() { - let networkError = AuthenticationError(info: [:], statusCode: 0) - let renewError = CredentialsManagerError(code: .renewFailed, cause: networkError) - let flutterError = FlutterError(from: renewError) - let details = flutterError.details as! [String: Any] - // AuthenticationError with statusCode 0 and empty info is treated as a network error - XCTAssertNotNil(details["_isRetryable"]) - } } From 5b232610cfdffae1792caef5f356e4d372c66fd7 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Thu, 26 Mar 2026 19:37:01 +0530 Subject: [PATCH 3/9] feat: expose isRetryable property on CredentialsManagerException, ApiException, and WebAuthenticationException --- auth0_flutter/EXAMPLES.md | 18 ++++++++-- .../web_auth/LoginWebAuthRequestHandler.kt | 3 +- .../web_auth/LogoutWebAuthRequestHandler.kt | 3 +- .../LoginWebAuthRequestHandlerTest.kt | 2 +- .../LogoutWebAuthRequestHandlerTest.kt | 2 +- .../CredentialsManagerExtensions.swift | 11 +++---- .../Classes/WebAuth/WebAuthExtensions.swift | 14 +++++++- .../CredentialsManagerExtensionsTests.swift | 9 ++--- .../example/ios/Tests/Utilities.swift | 6 ++++ .../WebAuth/WebAuthExtensionsTests.swift | 19 +++++++++++ .../lib/src/auth/api_exception.dart | 1 + .../web_authentication_exception.dart | 3 ++ .../test/api_exception_test.dart | 31 +++++++++++++++++ .../web_authentication_exception_test.dart | 33 +++++++++++++++++++ 14 files changed, 133 insertions(+), 22 deletions(-) diff --git a/auth0_flutter/EXAMPLES.md b/auth0_flutter/EXAMPLES.md index 6642ccb90..e82102342 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. +
@@ -540,7 +546,7 @@ try { } ``` -> This property is available on Android and iOS/macOS. On Android, it checks whether the underlying cause is a network-related `AuthenticationException`. On iOS/macOS, it checks for `AuthenticationError.isNetworkError` or `URLError`, and also returns `true` for biometrics failures. +> 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 @@ -912,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/request_handlers/web_auth/LoginWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt index e21e983e5..2a6aed9be 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,8 @@ class LoginWebAuthRequestHandler( builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), exception) + result.error(exception.getCode(), exception.getDescription(), + mapOf("_isRetryable" to exception.isNetworkError)) } 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 3e8f90a61..2f6fd1ccd 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,8 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), exception) + result.error(exception.getCode(), exception.getDescription(), + mapOf("_isRetryable" to exception.isNetworkError)) } override fun onSuccess(res: Void?) { 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 f24d6753e..bb19edb45 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,7 @@ 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 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 e0efb40a0..dff36916f 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 @@ -122,7 +122,7 @@ 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 diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift index bf9f39865..7fd8b6bf9 100644 --- a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift @@ -21,16 +21,13 @@ extension FlutterError { } let isRetryable: Bool - switch credentialsManagerError { - case .renewFailed: - if let authError = credentialsManagerError.cause as? AuthenticationError { + if let cause = credentialsManagerError.cause { + if let authError = cause as? AuthenticationError { isRetryable = authError.isNetworkError } else { - isRetryable = credentialsManagerError.cause is URLError + isRetryable = cause is URLError } - case .biometricsFailed: - isRetryable = true - default: + } else { isRetryable = false } diff --git a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift index fe7318a94..dba3266e0 100644 --- a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift @@ -20,7 +20,19 @@ 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: Bool + if let cause = webAuthError.cause { + if let authError = cause as? AuthenticationError { + isRetryable = authError.isNetworkError + } else { + isRetryable = cause is URLError + } + } else { + isRetryable = false + } + details["_isRetryable"] = isRetryable + self.init(code: code, message: String(describing: webAuthError), details: details) } } diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index 571916c31..82970b696 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -20,12 +20,13 @@ class CredentialsManagerExtensionsTests: XCTestCase { } } - func testIsRetryableIsFalseForNonRetryableErrors() { + func testIsRetryableIsFalseForErrorsWithoutNetworkCause() { let nonRetryableErrors: [CredentialsManagerError] = [ .noCredentials, .noRefreshToken, .renewFailed, .storeFailed, + .biometricsFailed, .revokeFailed, .largeMinTTL ] @@ -37,10 +38,4 @@ class CredentialsManagerExtensionsTests: XCTestCase { } } - func testIsRetryableIsTrueForBiometricsFailed() { - let flutterError = FlutterError(from: .biometricsFailed) - let details = flutterError.details as! [String: Any] - XCTAssertEqual(details["_isRetryable"] as? Bool, true) - } - } diff --git a/auth0_flutter/example/ios/Tests/Utilities.swift b/auth0_flutter/example/ios/Tests/Utilities.swift index 51e0a2f6f..2828969c5 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) { diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index 1b8fe238f..3fffc9758 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift @@ -21,4 +21,23 @@ 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_platform_interface/lib/src/auth/api_exception.dart b/auth0_flutter_platform_interface/lib/src/auth/api_exception.dart index bf03dcbde..02cd441d8 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/web-auth/web_authentication_exception.dart b/auth0_flutter_platform_interface/lib/src/web-auth/web_authentication_exception.dart index dcfa87c5c..8e3d0b401 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 7793de308..3cedc27f1 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/web_authentication_exception_test.dart b/auth0_flutter_platform_interface/test/web_authentication_exception_test.dart index 42eee9957..d81fb7973 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 5ef8447c92cba5c2a1ccc265f781c32106f699f7 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 30 Mar 2026 13:51:21 +0530 Subject: [PATCH 4/9] include cause in CredentialsManagerException details map and simplify isRetryable using Auth0APIError protocol --- .../CredentialsManagerExceptionExtensions.kt | 9 +++++++-- .../CredentialsManagerExtensions.swift | 11 +---------- .../darwin/Classes/WebAuth/WebAuthExtensions.swift | 11 +---------- 3 files changed, 9 insertions(+), 22 deletions(-) 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 index 259cea30c..0da90d1c1 100644 --- 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 @@ -4,10 +4,15 @@ import com.auth0.android.authentication.AuthenticationException import com.auth0.android.authentication.storage.CredentialsManagerException fun CredentialsManagerException.toMap(): Map { - val isRetryable = when (val exceptionCause = this.cause) { + val exceptionCause = this.cause + val isRetryable = when (exceptionCause) { is AuthenticationException -> exceptionCause.isNetworkError else -> false } - return mapOf("_isRetryable" to isRetryable) + val map = mutableMapOf("_isRetryable" to isRetryable) + if (exceptionCause != null) { + map["cause"] = exceptionCause.toString() + } + return map } diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift index 7fd8b6bf9..881b568bf 100644 --- a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift @@ -20,16 +20,7 @@ extension FlutterError { default: code = "UNKNOWN" } - let isRetryable: Bool - if let cause = credentialsManagerError.cause { - if let authError = cause as? AuthenticationError { - isRetryable = authError.isNetworkError - } else { - isRetryable = cause is URLError - } - } else { - isRetryable = false - } + let isRetryable = (credentialsManagerError.cause as? Auth0APIError)?.isNetworkError ?? false var errorDetails = credentialsManagerError.details errorDetails["_isRetryable"] = isRetryable diff --git a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift index dba3266e0..96b6de8c1 100644 --- a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift @@ -21,16 +21,7 @@ extension FlutterError { default: code = "UNKNOWN" } var details = webAuthError.details - let isRetryable: Bool - if let cause = webAuthError.cause { - if let authError = cause as? AuthenticationError { - isRetryable = authError.isNetworkError - } else { - isRetryable = cause is URLError - } - } else { - isRetryable = false - } + let isRetryable = (webAuthError.cause as? Auth0APIError)?.isNetworkError ?? false details["_isRetryable"] = isRetryable self.init(code: code, message: String(describing: webAuthError), details: details) } From 1c2503c2b6a44f85c4cc18cd253167c8b5276cdd Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 30 Mar 2026 14:13:36 +0530 Subject: [PATCH 5/9] feat: upgrade Auth0.swift to 2.18.0 and use Auth0APIError.isRetryable --- .../CredentialsManager/CredentialsManagerExtensions.swift | 2 +- auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift | 2 +- auth0_flutter/darwin/auth0_flutter.podspec | 2 +- auth0_flutter/ios/auth0_flutter.podspec | 2 +- auth0_flutter/macos/auth0_flutter.podspec | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift index 881b568bf..a3efc75ba 100644 --- a/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift +++ b/auth0_flutter/darwin/Classes/CredentialsManager/CredentialsManagerExtensions.swift @@ -20,7 +20,7 @@ extension FlutterError { default: code = "UNKNOWN" } - let isRetryable = (credentialsManagerError.cause as? Auth0APIError)?.isNetworkError ?? false + let isRetryable = (credentialsManagerError.cause as? Auth0APIError)?.isRetryable ?? false var errorDetails = credentialsManagerError.details errorDetails["_isRetryable"] = isRetryable diff --git a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift index 96b6de8c1..263c728ed 100644 --- a/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift +++ b/auth0_flutter/darwin/Classes/WebAuth/WebAuthExtensions.swift @@ -21,7 +21,7 @@ extension FlutterError { default: code = "UNKNOWN" } var details = webAuthError.details - let isRetryable = (webAuthError.cause as? Auth0APIError)?.isNetworkError ?? false + 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 5797d1292..830789925 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/ios/auth0_flutter.podspec b/auth0_flutter/ios/auth0_flutter.podspec index 5797d1292..830789925 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 5797d1292..830789925 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' From 92a6f5a6483e21a2d3ad89a8d3540366cea9fe5c Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 30 Mar 2026 15:28:49 +0530 Subject: [PATCH 6/9] include cause in WebAuth exception details map --- .../request_handlers/web_auth/LoginWebAuthRequestHandler.kt | 5 +++-- .../request_handlers/web_auth/LogoutWebAuthRequestHandler.kt | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) 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 2a6aed9be..08401eac8 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,8 +80,9 @@ class LoginWebAuthRequestHandler( builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), - mapOf("_isRetryable" to exception.isNetworkError)) + val details = mutableMapOf("_isRetryable" to exception.isNetworkError) + exception.cause?.let { details["cause"] = it.toString() } + 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 2f6fd1ccd..d1a73fe8f 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,8 +46,9 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { - result.error(exception.getCode(), exception.getDescription(), - mapOf("_isRetryable" to exception.isNetworkError)) + val details = mutableMapOf("_isRetryable" to exception.isNetworkError) + exception.cause?.let { details["cause"] = it.toString() } + result.error(exception.getCode(), exception.getDescription(), details) } override fun onSuccess(res: Void?) { From 12514a7da1c4ca8be2c99344222d61e7bf23b2ca Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Mon, 30 Mar 2026 16:03:45 +0530 Subject: [PATCH 7/9] adding causeStackTrace --- .../auth0_flutter/CredentialsManagerExceptionExtensions.kt | 1 + .../request_handlers/web_auth/LoginWebAuthRequestHandler.kt | 5 ++++- .../request_handlers/web_auth/LogoutWebAuthRequestHandler.kt | 5 ++++- 3 files changed, 9 insertions(+), 2 deletions(-) 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 index 0da90d1c1..1cfa8688b 100644 --- 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 @@ -13,6 +13,7 @@ fun CredentialsManagerException.toMap(): Map { 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/web_auth/LoginWebAuthRequestHandler.kt b/auth0_flutter/android/src/main/kotlin/com/auth0/auth0_flutter/request_handlers/web_auth/LoginWebAuthRequestHandler.kt index 08401eac8..eb8f764b2 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 @@ -81,7 +81,10 @@ class LoginWebAuthRequestHandler( builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { val details = mutableMapOf("_isRetryable" to exception.isNetworkError) - exception.cause?.let { details["cause"] = it.toString() } + exception.cause?.let { + details["cause"] = it.toString() + details["causeStackTrace"] = it.stackTraceToString() + } result.error(exception.getCode(), exception.getDescription(), details) } 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 d1a73fe8f..a120b1fff 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 @@ -47,7 +47,10 @@ class LogoutWebAuthRequestHandler(private val builderResolver: (MethodCallReques builder.start(context, object : Callback { override fun onFailure(exception: AuthenticationException) { val details = mutableMapOf("_isRetryable" to exception.isNetworkError) - exception.cause?.let { details["cause"] = it.toString() } + exception.cause?.let { + details["cause"] = it.toString() + details["causeStackTrace"] = it.stackTraceToString() + } result.error(exception.getCode(), exception.getDescription(), details) } From 371c4561248881473725bd620dec1ec67ba1b0e4 Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 31 Mar 2026 12:22:32 +0530 Subject: [PATCH 8/9] test: add tests for cause and causeStackTrace in exception details map --- ...edentialsManagerExceptionExtensionsTest.kt | 33 +++++++++++++++++++ .../LoginWebAuthRequestHandlerTest.kt | 29 ++++++++++++++++ .../LogoutWebAuthRequestHandlerTest.kt | 27 +++++++++++++++ .../CredentialsManagerExtensionsTests.swift | 15 +++++++++ .../WebAuth/WebAuthExtensionsTests.swift | 15 +++++++++ 5 files changed, 119 insertions(+) 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 index af92e8b22..94a3c6ea7 100644 --- 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 @@ -58,4 +58,37 @@ class CredentialsManagerExceptionExtensionsTest { 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 bb19edb45..d369e83e8 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 @@ -308,6 +308,35 @@ class LoginWebAuthRequestHandlerTest { 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() + `when`(exception.getCode()).thenReturn("code") + `when`(exception.getDescription()).thenReturn("description") + `when`(exception.isNetworkError).thenReturn(true) + `when`(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 fun `returns the result when the builder succeeds`() { val builder = mock() 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 dff36916f..bb06e36ab 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 @@ -125,6 +125,33 @@ class LogoutWebAuthRequestHandlerTest { 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() + `when`(exception.getCode()).thenReturn("code") + `when`(exception.getDescription()).thenReturn("description") + `when`(exception.isNetworkError).thenReturn(true) + `when`(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 fun `handler returns the result when the builder succeeds`() { val mockBuilder = mock() diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index 82970b696..b261aa76a 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -38,4 +38,19 @@ class CredentialsManagerExtensionsTests: XCTestCase { } } + func testCauseIsIncludedInDetailsWhenPresent() { + let cause = MockAuth0Error(debugDescription: "network error", cause: nil) + let error = CredentialsManagerError(code: .renewFailed, cause: cause) + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertEqual(details["cause"] as? String, String(describing: cause)) + } + + func testCauseIsNotIncludedInDetailsWhenAbsent() { + let error = CredentialsManagerError.noCredentials + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertNil(details["cause"]) + } + } diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index 3fffc9758..c6681a05f 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift @@ -40,4 +40,19 @@ class WebAuthExtensionsTests: XCTestCase { "Expected isRetryable to be false for \(error)") } } + + func testCauseIsIncludedInDetailsWhenPresent() { + let cause = MockAuth0Error(debugDescription: "network error", cause: nil) + let error = WebAuthError(code: .other, cause: cause) + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertEqual(details["cause"] as? String, String(describing: cause)) + } + + func testCauseIsNotIncludedInDetailsWhenAbsent() { + let error = WebAuthError.userCancelled + let flutterError = FlutterError(from: error) + let details = flutterError.details as! [String: Any] + XCTAssertNil(details["cause"]) + } } From 39d98488dbdd77eec40d1ecfa2a4bc92eca3b61d Mon Sep 17 00:00:00 2001 From: utkrishtS Date: Tue, 31 Mar 2026 13:11:49 +0530 Subject: [PATCH 9/9] fixing UT failures --- .../LoginWebAuthRequestHandlerTest.kt | 8 ++++---- .../LogoutWebAuthRequestHandlerTest.kt | 10 ++++++---- .../CredentialsManagerExtensionsTests.swift | 14 -------------- .../ios/Tests/WebAuth/WebAuthExtensionsTests.swift | 14 -------------- 4 files changed, 10 insertions(+), 36 deletions(-) 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 d369e83e8..6db3c70c2 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 @@ -314,10 +314,10 @@ class LoginWebAuthRequestHandlerTest { val mockResult = mock() val cause = RuntimeException("network error") val exception = mock() - `when`(exception.getCode()).thenReturn("code") - `when`(exception.getDescription()).thenReturn("description") - `when`(exception.isNetworkError).thenReturn(true) - `when`(exception.cause).thenReturn(cause) + 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) 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 bb06e36ab..5666f4911 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.* @@ -132,10 +134,10 @@ class LogoutWebAuthRequestHandlerTest { val handler = LogoutWebAuthRequestHandler { mockBuilder } val cause = RuntimeException("network error") val exception = mock() - `when`(exception.getCode()).thenReturn("code") - `when`(exception.getDescription()).thenReturn("description") - `when`(exception.isNetworkError).thenReturn(true) - `when`(exception.cause).thenReturn(cause) + 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) diff --git a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift index b261aa76a..3af6ade4e 100644 --- a/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/CredentialsManager/CredentialsManagerExtensionsTests.swift @@ -38,19 +38,5 @@ class CredentialsManagerExtensionsTests: XCTestCase { } } - func testCauseIsIncludedInDetailsWhenPresent() { - let cause = MockAuth0Error(debugDescription: "network error", cause: nil) - let error = CredentialsManagerError(code: .renewFailed, cause: cause) - let flutterError = FlutterError(from: error) - let details = flutterError.details as! [String: Any] - XCTAssertEqual(details["cause"] as? String, String(describing: cause)) - } - - func testCauseIsNotIncludedInDetailsWhenAbsent() { - let error = CredentialsManagerError.noCredentials - let flutterError = FlutterError(from: error) - let details = flutterError.details as! [String: Any] - XCTAssertNil(details["cause"]) - } } diff --git a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift index c6681a05f..4bef3c43c 100644 --- a/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift +++ b/auth0_flutter/example/ios/Tests/WebAuth/WebAuthExtensionsTests.swift @@ -41,18 +41,4 @@ class WebAuthExtensionsTests: XCTestCase { } } - func testCauseIsIncludedInDetailsWhenPresent() { - let cause = MockAuth0Error(debugDescription: "network error", cause: nil) - let error = WebAuthError(code: .other, cause: cause) - let flutterError = FlutterError(from: error) - let details = flutterError.details as! [String: Any] - XCTAssertEqual(details["cause"] as? String, String(describing: cause)) - } - - func testCauseIsNotIncludedInDetailsWhenAbsent() { - let error = WebAuthError.userCancelled - let flutterError = FlutterError(from: error) - let details = flutterError.details as! [String: Any] - XCTAssertNil(details["cause"]) - } }