Skip to content
Merged
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
2 changes: 1 addition & 1 deletion FronteggRN.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Pod::Spec.new do |s|
if defined?(spm_dependency)
spm_dependency(s,
url: 'https://github.com/frontegg/frontegg-ios-swift.git',
requirement: { kind: 'exactVersion', version: '1.3.17' },
requirement: { kind: 'exactVersion', version: '1.3.21' },
products: ['FronteggSwift']
)
end
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ dependencies {
implementation "androidx.browser:browser:1.8.0"
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.frontegg.sdk:android:1.3.36'
implementation 'com.frontegg.sdk:android:1.3.41'
testImplementation "junit:junit:4.13.2"
}

Expand Down
33 changes: 33 additions & 0 deletions android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import com.frontegg.android.exceptions.CanceledByUserException
import com.frontegg.android.exceptions.FailedToAuthenticateException
import com.frontegg.android.fronteggAuth
import com.frontegg.android.models.Entitlement
import com.frontegg.android.embedded.LoginBoxCustomization
import com.frontegg.android.services.FronteggInnerStorage
import java.io.IOException
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
Expand Down Expand Up @@ -158,6 +160,37 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) :
}
}

/**
* Login with runtime login-box overrides. Kept separate from [login] so a JS bundle
* carrying this feature still runs against an older native binary: the JS falls back to
* [login] when this method is absent, losing the theming rather than the sign-in.
*/
@ReactMethod
fun loginWithOptions(loginHint: String?, customization: ReadableMap?, promise: Promise) {
withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity ->
applyLoginBoxCustomization(customization)
auth.login(activity, loginHint) { error ->
resolveOrRejectLogin(error, promise)
}
}
}

@ReactMethod
fun isLoginBoxCustomizationSupported(promise: Promise) {
promise.resolve(LoginBoxCustomization.isSupported())
}

/**
* Every call fully determines the login box appearance: a key the caller omitted is
* cleared rather than inherited, so one brand's theme cannot leak into another brand's
* login.
*/
private fun applyLoginBoxCustomization(customization: ReadableMap?) {
val storage = FronteggInnerStorage()
storage.loginBoxThemeOptions = customization?.getMap("themeOptions")?.toHashMap()
storage.loginBoxLocalizations = customization?.getMap("localizations")?.toHashMap()
}

@ReactMethod
fun switchTenant(tenantId: String, promise: Promise) {
auth.switchTenant(tenantId) { success ->
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Passkeys provide a seamless, passwordless login experience using WebAuthn and pl
```groovy
dependencies {
implementation 'androidx.browser:browser:1.8.0'
implementation 'com.frontegg.sdk:android:1.3.35'
implementation 'com.frontegg.sdk:android:1.3.41'
}
```

Expand Down
55 changes: 54 additions & 1 deletion docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const { isAuthenticated, user, isLoading } = useAuth();

| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `login` | `loginHint?: string` | `Promise<void>` | Opens the login flow. `loginHint` pre-fills the identifier field. Resolves when login completes; rejects with a `FronteggLoginError`. |
| `login` | `loginHint?: string`<br>or `options?: LoginOptions` | `Promise<void>` | Opens the login flow. `loginHint` pre-fills the identifier field. Resolves when login completes; rejects with a `FronteggLoginError`. See [Login box customization](#login-box-customization) for `LoginOptions`. |
| `logout` | None | `Promise<void>` | Signs the user out and clears stored credentials. |
| `loginWithPasskeys` | None | `Promise<void>` | Signs in with a passkey. Needs iOS 15+ or Android API 26+. |
| `registerPasskeys` | None | `Promise<void>` | Registers a passkey for the signed-in user. |
Expand All @@ -54,6 +54,59 @@ const { isAuthenticated, user, isLoading } = useAuth();
> `directLoginAction` does not accept them yet and ignores them. `ephemeralSession` is inherently
> iOS-specific — it maps to the `ASWebAuthenticationSession` browser session.


## Login box customization

`login()` accepts a `LoginOptions` object instead of a bare login hint, letting the app
theme and re-word the embedded login box at runtime — for a multi-brand app whose
appearance is resolved per brand and so cannot be expressed as static per-environment
configuration.

```ts
await login({
loginHint: 'user@example.com',
themeOptions: {
loginBox: {
palette: { primary: { main: '#3F6655' } },
logo: { image: 'https://example.com/logo.png' },
},
},
localizations: {
en: { loginBox: { login: { title: 'Sign-in', continue: 'Log In' } } },
},
});
```

| Field | Type | Description |
|-------|------|-------------|
| `loginHint` | `string` | Pre-fills the identifier field. |
| `themeOptions` | `Record<string, unknown>` | Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`. Omitted means cleared. |
| `localizations` | `Record<string, unknown>` | Same shape as `localizations` from the same endpoint. Omitted means cleared. |

Values are deep-merged over the environment's configuration, so keys the override does not
mention keep whatever the environment defines.

Each `login()` call fully determines the login box appearance: a field you omit is cleared
rather than carried over, so one brand's theme cannot appear on another brand's login. Pass
both fields every time you want both applied.

Embedded mode only — hosted mode runs outside the app's WebView, so there is no
injection point.

Android WebView providers without `DOCUMENT_START_SCRIPT` cannot apply overrides, and the
box falls back to the environment's own branding. Call `isLoginBoxCustomizationSupported()`
before relying on per-brand appearance:

```ts
if (!(await isLoginBoxCustomizationSupported())) {
// fall back to hosted login, or to your own branded screen
}
```

| Method | Parameters | Returns | Description |
|--------|------------|---------|-------------|
| `isLoginBoxCustomizationSupported` | None | `Promise<boolean>` | Whether this device can apply login box overrides. Always `true` on iOS; `false` on Android WebViews lacking `DOCUMENT_START_SCRIPT`, and on native binaries older than this feature. |

## Step-up authentication

| Method | Parameters | Returns | Description |
Expand Down
2 changes: 1 addition & 1 deletion docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ yarn ios --simulator "iPhone 17 Pro Max"

If a physical iPhone is connected, React Native may target the device and fail on code signing (`com.your.bundle`). Use `--simulator` or disconnect the device.

The `frontegg_spm` helper pins **FronteggSwift 1.3.11** from `https://github.com/frontegg/frontegg-ios-swift.git`, links it to the **FronteggRN** CocoaPods target, and configures the app Xcode project so SPM resolves without duplicate symbol errors at link time.
The `frontegg_spm` helper pins **FronteggSwift 1.3.21** from `https://github.com/frontegg/frontegg-ios-swift.git`, links it to the **FronteggRN** CocoaPods target, and configures the app Xcode project so SPM resolves without duplicate symbol errors at link time.

> **Embedded mode step-up:** `FronteggSwift` **1.3.11** routes `stepUp()` through the embedded `WKWebView` when `embeddedMode` is enabled (instead of `ASWebAuthenticationSession`), so MFA/step-up reuses the existing web session. Set `<key>embeddedMode</key><true/>` in `Frontegg.plist` if you use the embedded login box.

Expand Down
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ android {
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
implementation 'com.frontegg.sdk:android:1.3.35'
implementation 'com.frontegg.sdk:android:1.3.41'
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions ios/FronteggRN.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter)
resolver: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
loginWithOptions: (NSString *)loginHint
customization: (NSDictionary *)customization
resolver: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
isLoginBoxCustomizationSupported: (RCTPromiseResolveBlock)resolve
rejecter: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
switchTenant: (NSString *)tenantId
resolver: (RCTPromiseResolveBlock)resolve
Expand Down
39 changes: 39 additions & 0 deletions ios/FronteggRN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,39 @@ class FronteggRN: RCTEventEmitter {
_ loginHint: String?,
resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock
) -> Void {
performLogin(loginHint, customization: nil, applyCustomization: false, resolver: resolver, rejecter: rejecter)
}

/// Login with runtime login-box overrides. Kept separate from ``login`` so a JS bundle
/// carrying this feature still runs against an older native binary: the JS falls back
/// to ``login`` when this method is absent, losing the theming rather than the sign-in.
@objc
func loginWithOptions(
_ loginHint: String?,
customization: NSDictionary?,
resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock
) -> Void {
performLogin(loginHint, customization: customization, applyCustomization: true, resolver: resolver, rejecter: rejecter)
}

@objc
func isLoginBoxCustomizationSupported(
_ resolve: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock
) -> Void {
resolve(true)
}

private func performLogin(
_ loginHint: String?,
customization: NSDictionary?,
applyCustomization: Bool,
resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock
) -> Void {

DispatchQueue.main.sync {
if applyCustomization {
Self.applyLoginBoxCustomization(customization)
}
let completion: FronteggAuth.CompletionHandler = { result in
switch(result) {
case .success(_):
Expand Down Expand Up @@ -211,6 +242,14 @@ class FronteggRN: RCTEventEmitter {
}
}

/// Every call fully determines the login box appearance: a key the caller omitted is
/// cleared rather than inherited, so one brand's theme cannot leak into another
/// brand's login.
private static func applyLoginBoxCustomization(_ customization: NSDictionary?) {
FronteggApp.shared.loginBoxThemeOptions = customization?["themeOptions"] as? [String: Any]
FronteggApp.shared.loginBoxLocalizations = customization?["localizations"] as? [String: Any]
}

/// Maps a FronteggError from login() onto the stable, cross-platform rejection codes
/// documented for the JS `login()` (issue #110). Keep in sync with
/// `FronteggLoginErrorCode` in src/FronteggNative.ts and the Android bridge.
Expand Down
2 changes: 1 addition & 1 deletion ios/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
],
products: [],
dependencies: [
.package(url: "https://github.com/frontegg/frontegg-ios-swift.git", exact: "1.3.17"),
.package(url: "https://github.com/frontegg/frontegg-ios-swift.git", exact: "1.3.21"),
],
targets: []
)
4 changes: 2 additions & 2 deletions ios/frontegg_spm.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true

# Links FronteggSwift (SPM 1.3.17) to the FronteggRN CocoaPods target after `pod install`.
# Links FronteggSwift (SPM 1.3.21) to the FronteggRN CocoaPods target after `pod install`.
# Patches Pods.xcodeproj/project.pbxproj as text — xcodeproj gem save() breaks Xcode 26.
module FronteggSPM
PACKAGE_URL = 'https://github.com/frontegg/frontegg-ios-swift.git'
PACKAGE_VERSION = '1.3.17'
PACKAGE_VERSION = '1.3.21'
PRODUCT_NAME = 'FronteggSwift'

PACKAGE_REF_ID = '2F0A5C48A15A74750BE517A0'
Expand Down
88 changes: 87 additions & 1 deletion src/FronteggNative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,107 @@
return error;
}

/**
* Runtime overrides for the embedded login box (issue #127).
*
* Login-box configuration is scoped to a Frontegg environment, which cannot
* express appearance that is only known at runtime — for example a multi-brand
* app that resolves each brand's logo and colours from its own backend. These
* options are deep-merged over the environment's configuration, so keys left
* unset keep whatever the environment already defines.
*
* Pass `null` for either key to clear a previously set override and fall back to
* the environment's own configuration. Omitting a key leaves it unchanged.
*
* Embedded mode only; hosted mode runs outside the app's WebView.
*/
export interface LoginBoxCustomization {
/**
* Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`,
* e.g. `{ loginBox: { palette: { primary: { main: '#3F6655' } } } }`.
*/
themeOptions?: Record<string, unknown> | null;
/**
* Same shape as `localizations` from `/frontegg/metadata?entityName=adminBox`,
* e.g. `{ en: { loginBox: { login: { title: 'Sign-in' } } } }`.
*/
localizations?: Record<string, unknown> | null;
}

/** Options accepted by {@link login}. */
export interface LoginOptions extends LoginBoxCustomization {
/** Pre-fills the email field on the login box. */
loginHint?: string;
}

/**
* Opens the Frontegg login flow. Resolves when login completes successfully;
* rejects with a {@link FronteggLoginError} when it fails or is cancelled.
*
* Accepts either a login hint (the original signature) or a {@link LoginOptions}
* object:
*
* ```ts
* await login('user@example.com');
* await login({
* loginHint: 'user@example.com',
* themeOptions: { loginBox: { palette: { primary: { main: '#3F6655' } } } },
* });
* ```
*/
export async function login(loginHint?: string): Promise<void> {
export async function login(
loginHintOrOptions?: string | LoginOptions
): Promise<void> {
const options: LoginOptions =
typeof loginHintOrOptions === 'string'
? { loginHint: loginHintOrOptions }
: loginHintOrOptions ?? {};

const { loginHint } = options;

// Every call fully determines the login box appearance. A key the caller omitted is
// sent as null and clears any previous value, so one brand's theme cannot leak into
// the next brand's login.
const customization = {
themeOptions: options.themeOptions ?? null,
localizations: options.localizations ?? null,
};
const customized =
customization.themeOptions !== null || customization.localizations !== null;

// 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.
try {
if (typeof FronteggRN.loginWithOptions === 'function') {
return await FronteggRN.loginWithOptions(loginHint, customization);
}

// Older native binary (a JS-only update). Sign-in still works; only the overrides
// are unavailable, so say so rather than failing the login.
if (customized) {
console.warn(
'[frontegg] login box customization needs a newer native SDK; signing in without it'
);
}
return await FronteggRN.login(loginHint);
} catch (e) {
throw normalizeLoginError(e);
}
}

/**
* Whether this device can apply login box overrides. Android WebView providers without
* `DOCUMENT_START_SCRIPT` cannot, and the box then renders the environment's own
* branding — check this before relying on per-brand appearance. Always true on iOS.
*/
export async function isLoginBoxCustomizationSupported(): Promise<boolean> {
if (typeof FronteggRN.isLoginBoxCustomizationSupported !== 'function') {
return false;
}
return FronteggRN.isLoginBoxCustomizationSupported();
}

export function logout(): Promise<void> {
return FronteggRN.logout();
}
Expand Down Expand Up @@ -174,7 +260,7 @@
}

export async function requestAuthorize(
refreshToken: string,

Check warning on line 263 in src/FronteggNative.ts

View workflow job for this annotation

GitHub Actions / Lint and Typecheck

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