diff --git a/android/src/main/kotlin/com/frontegg/flutter/FronteggMethodCallHandler.kt b/android/src/main/kotlin/com/frontegg/flutter/FronteggMethodCallHandler.kt index 4c0fac9..8bae149 100644 --- a/android/src/main/kotlin/com/frontegg/flutter/FronteggMethodCallHandler.kt +++ b/android/src/main/kotlin/com/frontegg/flutter/FronteggMethodCallHandler.kt @@ -109,8 +109,8 @@ class FronteggMethodCallHandler( val data = call.argument("data") ?: throw ArgumentNotFoundException("data") if (context.fronteggAuth.isEmbeddedMode) { activityProvider.getActivity()?.let { - context.fronteggAuth.directLoginAction(it, type, data) { - result.success(null) + context.fronteggAuth.directLoginAction(it, type, data) { error -> + completeAuthResult(error, result) { result.success(null) } } } } else { @@ -230,8 +230,8 @@ class FronteggMethodCallHandler( activity = activity, type = "social-login", data = provider, - callback = { - result.success(null) + callback = { error -> + completeAuthResult(error, result) { result.success(null) } } ) } @@ -260,8 +260,8 @@ class FronteggMethodCallHandler( activity = activity, type = "custom-social-login", data = id, - callback = { - result.success(null) + callback = { error -> + completeAuthResult(error, result) { result.success(null) } } ) } @@ -350,15 +350,8 @@ class FronteggMethodCallHandler( context.fronteggAuth.login( activity = it, loginHint = loginHint, - callback = { - // Force state update after successful authentication - // This is especially important for hosted mode - GlobalScope.launch(Dispatchers.Main) { - // Trigger state listener to update Flutter - // The state listener will automatically detect changes - // and send updated state to Flutter - } - result.success(null) + callback = { error -> + completeAuthResult(error, result) { result.success(null) } } ) } @@ -475,4 +468,28 @@ class FronteggMethodCallHandler( AdminPortalActivity.open(activity) result.success(null) } -} \ No newline at end of file +} + +/** + * Routes an auth callback's optional error to the Flutter [result] (FR-25942). The native + * `login`/`directLoginAction` callbacks are `((Exception?) -> Unit)?`; the plugin used to ignore + * the error and always call `result.success(null)`, so a cancelled or failed authentication + * looked like success to Dart. On a non-null error the result is completed with `result.error(...)`; + * only when there is no error does [onSuccess] run. Top-level so it can be unit-tested without a + * Context/Activity (which can't be mocked in this toolchain). + */ +internal fun completeAuthResult( + error: Exception?, + result: MethodChannel.Result, + onSuccess: () -> Unit, +) { + if (error == null) { + onSuccess() + return + } + if (error is FronteggException) { + result.error(error.message ?: "unknown", error.message ?: "Authentication failed", null) + } else { + result.error("unknown", error.localizedMessage ?: "Authentication failed", null) + } +} diff --git a/android/src/test/kotlin/com/frontegg/flutter/AuthResultPropagationTest.kt b/android/src/test/kotlin/com/frontegg/flutter/AuthResultPropagationTest.kt new file mode 100644 index 0000000..538f4d7 --- /dev/null +++ b/android/src/test/kotlin/com/frontegg/flutter/AuthResultPropagationTest.kt @@ -0,0 +1,62 @@ +package com.frontegg.flutter + +import com.frontegg.android.exceptions.FronteggException +import io.flutter.plugin.common.MethodChannel +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * FR-25942: login/directLoginAction native callbacks are `((Exception?) -> Unit)?`, but the + * plugin ignored the `Exception?` and always called `result.success(null)`. A cancelled or + * failed authentication therefore looked like success to Dart. `completeAuthResult` must route + * a non-null error through `result.error(...)` and only call `onSuccess` when there is no error. + */ +internal class AuthResultPropagationTest { + + private class RecordingResult : MethodChannel.Result { + var errorCode: String? = null + var errorMessage: String? = null + var successCalled = false + override fun success(result: Any?) { successCalled = true } + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + this.errorCode = errorCode + this.errorMessage = errorMessage + } + override fun notImplemented() {} + } + + @Test + fun nullError_callsOnSuccess_notError() { + val result = RecordingResult() + var onSuccessRan = false + + completeAuthResult(null, result) { onSuccessRan = true } + + assertTrue(onSuccessRan, "onSuccess must run when there is no error") + assertNull(result.errorCode, "result.error must not be called on success") + } + + @Test + fun genericError_completesResultWithError_andSkipsOnSuccess() { + val result = RecordingResult() + var onSuccessRan = false + + completeAuthResult(RuntimeException("boom"), result) { onSuccessRan = true } + + assertFalse(onSuccessRan, "onSuccess must not run when the callback reports an error") + assertEquals("unknown", result.errorCode) + assertEquals("boom", result.errorMessage) + } + + @Test + fun fronteggException_usesItsMessageAsErrorCode() { + val result = RecordingResult() + + completeAuthResult(FronteggException("frontegg.error.mfa_required"), result) {} + + assertEquals("frontegg.error.mfa_required", result.errorCode) + } +} diff --git a/android/src/test/kotlin/com/frontegg/flutter/FronteggFlutterPluginTest.kt b/android/src/test/kotlin/com/frontegg/flutter/FronteggFlutterPluginTest.kt deleted file mode 100644 index e3a96ae..0000000 --- a/android/src/test/kotlin/com/frontegg/flutter/FronteggFlutterPluginTest.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.frontegg.flutter - -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import kotlin.test.Test -import org.mockito.Mockito - -/* - * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. - * - * Once you have built the plugin's example app, you can run these tests from the command - * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or - * you can run them directly from IDEs that support JUnit such as Android Studio. - */ - -internal class FronteggFlutterPluginTest { - @Test - fun onMethodCall_getPlatformVersion_returnsExpectedValue() { - val plugin = FronteggFlutterPlugin() - - val call = MethodCall("getPlatformVersion", null) - val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) - plugin.onMethodCall(call, mockResult) - - Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) - } -} diff --git a/ios/frontegg_flutter/Sources/frontegg_flutter/FronteggMethodCallHandler.swift b/ios/frontegg_flutter/Sources/frontegg_flutter/FronteggMethodCallHandler.swift index 8482022..ecce2eb 100644 --- a/ios/frontegg_flutter/Sources/frontegg_flutter/FronteggMethodCallHandler.swift +++ b/ios/frontegg_flutter/Sources/frontegg_flutter/FronteggMethodCallHandler.swift @@ -116,8 +116,13 @@ class FronteggMethodCallHandler { let additionalQueryParams = arguments["additionalQueryParams"] as? [String: String] ?? [:] - let compelation: FronteggAuth.CompletionHandler = { _ in - result(nil) + let compelation: FronteggAuth.CompletionHandler = { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } fronteggApp.auth.directLoginAction( @@ -147,8 +152,13 @@ class FronteggMethodCallHandler { let additionalQueryParams = arguments["additionalQueryParams"] as? [String: String] ?? [:] - let compelation: FronteggAuth.CompletionHandler = { _ in - result(nil) + let compelation: FronteggAuth.CompletionHandler = { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } fronteggApp.auth.directLoginAction( @@ -179,8 +189,13 @@ class FronteggMethodCallHandler { let additionalQueryParams = arguments["additionalQueryParams"] as? [String: String] ?? [:] - let compelation: FronteggAuth.CompletionHandler = { _ in - result(nil) + let compelation: FronteggAuth.CompletionHandler = { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } fronteggApp.auth.directLoginAction( @@ -212,8 +227,13 @@ class FronteggMethodCallHandler { let additionalQueryParams = arguments["additionalQueryParams"] as? [String: String] ?? [:] - let compelation: FronteggAuth.CompletionHandler = { _ in - result(nil) + let compelation: FronteggAuth.CompletionHandler = { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } fronteggApp.auth.directLoginAction( @@ -312,8 +332,13 @@ class FronteggMethodCallHandler { } private func logout(result: @escaping FlutterResult) { - fronteggApp.auth.logout() { _ in - result(nil) + fronteggApp.auth.logout() { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } } @@ -329,8 +354,13 @@ class FronteggMethodCallHandler { return result(FlutterError(code: "MISSING_PARAMS", message: "Missing 'tenantId' argumant", details: nil)) } - fronteggApp.auth.switchTenant(tenantId: tenantId) { _ in - result(nil) + fronteggApp.auth.switchTenant(tenantId: tenantId) { res in + switch (res) { + case .success(_): + result(nil) + case .failure(let error): + result(FlutterError(code: error.failureReason ?? "unknown", message: error.localizedDescription, details: nil)) + } } }