Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
22 changes: 15 additions & 7 deletions ios/FronteggRN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
}

Expand Down
13 changes: 5 additions & 8 deletions src/FronteggNative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,11 @@
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<void> {
// 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() {
Expand Down Expand Up @@ -70,7 +67,7 @@
}

export async function requestAuthorize(
refreshToken: string,

Check warning on line 70 in src/FronteggNative.ts

View workflow job for this annotation

GitHub Actions / Lint | Typecheck

'refreshToken' is already declared in the upper scope on line 39 column 23
deviceTokenCookie?: string
) {
return await FronteggRN.requestAuthorize(refreshToken, deviceTokenCookie);
Expand Down
Loading