diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index bd3b742..4475bb8 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -126,16 +126,16 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : @ReactMethod fun login(loginHint: String?, promise: Promise) { withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> - auth.login(activity, loginHint) { - promise.resolve("") + auth.login(activity, loginHint) { error -> + resolveOrRejectLogin(error, promise) } } } @ReactMethod fun switchTenant(tenantId: String, promise: Promise) { - auth.switchTenant(tenantId) { - promise.resolve(tenantId) + auth.switchTenant(tenantId) { success -> + resolveTenantSwitch(success, tenantId, promise) } } @@ -299,3 +299,29 @@ internal inline fun withActivityOrReject( } block(activity) } + +/** + * Completes [promise] for the native login callback (FR-25938). The SDK callback is + * `((Exception?) -> Unit)?`; the module used to ignore the error and always resolve, so a + * cancelled/failed login looked like success to JS. Reject on a non-null [error], resolve otherwise. + */ +internal fun resolveOrRejectLogin(error: Exception?, promise: Promise) { + if (error != null) { + promise.reject("LOGIN_ERROR", error.message ?: "Login failed", error) + } else { + promise.resolve("") + } +} + +/** + * Completes [promise] for the native switchTenant callback (FR-25938). The SDK callback yields a + * `Boolean`; the module used to ignore it and always resolve the tenant id, so a failed switch + * looked like success. Reject when [success] is false, otherwise resolve [tenantId]. + */ +internal fun resolveTenantSwitch(success: Boolean, tenantId: String, promise: Promise) { + if (success) { + promise.resolve(tenantId) + } else { + promise.reject("SWITCH_TENANT_ERROR", "Failed to switch tenant") + } +} diff --git a/android/src/test/java/com/frontegg/reactnative/AuthResultPropagationTest.kt b/android/src/test/java/com/frontegg/reactnative/AuthResultPropagationTest.kt new file mode 100644 index 0000000..b346fee --- /dev/null +++ b/android/src/test/java/com/frontegg/reactnative/AuthResultPropagationTest.kt @@ -0,0 +1,67 @@ +package com.frontegg.reactnative + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.WritableMap +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Test + +/** + * FR-25938: on Android, `login`'s callback ignored the `Exception?` arg and always resolved `""`, + * and `switchTenant` ignored the SDK callback's `Boolean` and always resolved the tenant id — so a + * cancelled login or a failed tenant switch looked like success to JS. The extracted helpers must + * reject on failure and resolve only on success. + */ +class AuthResultPropagationTest { + + private class RecordingPromise : Promise { + var rejectCode: String? = null + var resolvedValue: Any? = null + var resolved = false + override fun resolve(value: Any?) { resolved = true; resolvedValue = value } + override fun reject(code: String, message: String?) { rejectCode = code } + override fun reject(code: String, throwable: Throwable?) { rejectCode = code } + override fun reject(code: String, message: String?, throwable: Throwable?) { rejectCode = code } + override fun reject(throwable: Throwable) { rejectCode = "throwable" } + override fun reject(throwable: Throwable, userInfo: WritableMap) { rejectCode = "throwable" } + override fun reject(code: String, userInfo: WritableMap) { rejectCode = code } + override fun reject(code: String, throwable: Throwable?, userInfo: WritableMap) { rejectCode = code } + override fun reject(code: String, message: String?, userInfo: WritableMap) { rejectCode = code } + override fun reject(code: String, message: String?, throwable: Throwable?, userInfo: WritableMap) { rejectCode = code } + @Deprecated("Deprecated in Java") + override fun reject(message: String) { rejectCode = message } + } + + @Test + fun login_nullError_resolves() { + val promise = RecordingPromise() + resolveOrRejectLogin(null, promise) + assertEquals(true, promise.resolved) + assertNull(promise.rejectCode) + } + + @Test + fun login_error_rejects_andDoesNotResolve() { + val promise = RecordingPromise() + resolveOrRejectLogin(RuntimeException("cancelled"), promise) + assertFalse("must not resolve on a login failure", promise.resolved) + assertEquals("LOGIN_ERROR", promise.rejectCode) + } + + @Test + fun switchTenant_success_resolvesTenantId() { + val promise = RecordingPromise() + resolveTenantSwitch(true, "tenant-42", promise) + assertEquals("tenant-42", promise.resolvedValue) + assertNull(promise.rejectCode) + } + + @Test + fun switchTenant_failure_rejects_andDoesNotResolve() { + val promise = RecordingPromise() + resolveTenantSwitch(false, "tenant-42", promise) + assertFalse("must not resolve when the tenant switch fails", promise.resolved) + assertEquals("SWITCH_TENANT_ERROR", promise.rejectCode) + } +} diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index a3c530f..c90ea62 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -119,17 +119,18 @@ class FronteggRN: RCTEventEmitter { @objc func login( _ loginHint: String?, - resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock + resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) -> Void { - + DispatchQueue.main.sync { let completion: FronteggAuth.CompletionHandler = { result in switch(result) { case .success(_): resolver("Success") case .failure(let error): - resolver("Failed: \(error.failureReason ?? "")") - + // FR-25938: previously resolved "Failed: …", so a cancelled/failed login looked + // like success to JS. Reject so the awaited login() rejects. + rejecter(error.failureReason, error.localizedDescription, error) } } fronteggApp.auth.login(completion, loginHint:loginHint) @@ -140,10 +141,17 @@ class FronteggRN: RCTEventEmitter { @objc func switchTenant( _ tenantId: String, - resolver: @escaping RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock + resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) -> Void { - fronteggApp.auth.switchTenant(tenantId: tenantId) { _ in - resolver(tenantId) + fronteggApp.auth.switchTenant(tenantId: tenantId) { result in + switch result { + case .success(_): + resolver(tenantId) + case .failure(let error): + // FR-25938: previously ignored the result and always resolved, so a failed switch + // looked like success. + rejecter(error.failureReason, error.localizedDescription, error) + } } } diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index 8783ee2..f910f84 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -21,14 +21,11 @@ export function getConstants() { return FronteggRN.getConstants(); } -export function login(loginHint?: string) { - FronteggRN.login(loginHint) - .then((data: any) => { - console.log(data); - }) - .catch((e: any) => { - console.log(e); - }); +export async function login(loginHint?: string): Promise { + // FR-25938: previously fire-and-forget (swallowed the result in console.log), so callers could + // neither await completion nor observe a cancelled/failed login. Return the promise so it is + // awaitable and rejections propagate. + return FronteggRN.login(loginHint); } export function logout() {