From 59ce233771f338ea3f46a3b1de8525c7304409b7 Mon Sep 17 00:00:00 2001 From: Adam Rowe Date: Tue, 1 Sep 2026 13:31:48 -0400 Subject: [PATCH 1/5] feat: accept login box theme and localization overrides in login() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #127 on the JS side. login() now takes either a login hint (unchanged) or a LoginOptions object carrying themeOptions/localizations, forwarded to the native SDK as a customization payload and deep-merged over the environment's login-box configuration. await login({ loginHint: 'user@example.com', themeOptions: { loginBox: { palette: { primary: { main: '#3F6655' } } } }, }) Login-box configuration is scoped to a Frontegg environment, which cannot express appearance only known at runtime — a multi-brand app resolving each brand's logo and colours from its own backend has no per-application axis to configure. The web SDK already accepts these as FronteggProvider props. iOS forwards to FronteggApp.loginBoxThemeOptions/.loginBoxLocalizations (frontegg/frontegg-ios-swift#320). Android accepts the argument so the JS API is identical across platforms but does not yet apply it — that needs a matching change in frontegg-android-kotlin's EmbeddedAuthActivity. The payload is omitted entirely when nothing is customized, so behaviour is unchanged for existing callers. Tests: 6 new cases covering the string form, the options form, partial customization, and omission. 13 passed. --- .../frontegg/reactnative/FronteggRNModule.kt | 12 +++- ios/FronteggRN.m | 1 + ios/FronteggRN.swift | 16 +++++ src/FronteggNative.ts | 59 ++++++++++++++++++- src/__tests__/index.test.tsx | 57 +++++++++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index 27d08c1..616e675 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -1,6 +1,7 @@ package com.frontegg.reactnative import android.app.Activity +import android.util.Log import android.os.Handler import android.os.Looper import com.facebook.react.bridge.Arguments @@ -149,8 +150,17 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } + /** + * @param customization issue #127 login-box theme/copy overrides. Accepted so the + * JS API is identical across platforms, but not yet applied: the Android SDK has + * no equivalent of the iOS `FronteggApp.loginBoxThemeOptions`. Wiring it needs a + * matching change in frontegg-android-kotlin's EmbeddedAuthActivity. + */ @ReactMethod - fun login(loginHint: String?, promise: Promise) { + fun login(loginHint: String?, customization: ReadableMap?, promise: Promise) { + if (customization != null) { + Log.w("FronteggRN", "loginBox customization is not yet supported on Android; ignoring") + } withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> auth.login(activity, loginHint) { error -> resolveOrRejectLogin(error, promise) diff --git a/ios/FronteggRN.m b/ios/FronteggRN.m index 159da8c..1de8638 100644 --- a/ios/FronteggRN.m +++ b/ios/FronteggRN.m @@ -11,6 +11,7 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter) ) RCT_EXTERN_METHOD( login: (NSString *)loginHint + customization: (NSDictionary *)customization resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index da09506..341d2b2 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -178,10 +178,12 @@ class FronteggRN: RCTEventEmitter { @objc func login( _ loginHint: String?, + customization: NSDictionary?, resolver: @escaping RCTPromiseResolveBlock, rejecter: @escaping RCTPromiseRejectBlock ) -> Void { DispatchQueue.main.sync { + Self.applyLoginBoxCustomization(customization) let completion: FronteggAuth.CompletionHandler = { result in switch(result) { case .success(_): @@ -211,6 +213,20 @@ class FronteggRN: RCTEventEmitter { } } + /// Forwards issue #127 login-box overrides to the native SDK. Only assigns + /// when a key is present, so a caller passing just `themeOptions` does not + /// clear previously set localizations. + private static func applyLoginBoxCustomization(_ customization: NSDictionary?) { + guard let customization else { return } + + if let themeOptions = customization["themeOptions"] as? [String: Any] { + FronteggApp.shared.loginBoxThemeOptions = themeOptions + } + if let localizations = customization["localizations"] as? [String: Any] { + FronteggApp.shared.loginBoxLocalizations = localizations + } + } + /// 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. diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index 96ae1f4..10d9bc3 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -120,16 +120,71 @@ export function normalizeLoginError(e: unknown): FronteggLoginError { 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. + * + * 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; + /** + * Same shape as `localizations` from `/frontegg/metadata?entityName=adminBox`, + * e.g. `{ en: { loginBox: { login: { title: 'Sign-in' } } } }`. + */ + localizations?: Record; +} + +/** 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 { +export async function login( + loginHintOrOptions?: string | LoginOptions +): Promise { + const options: LoginOptions = + typeof loginHintOrOptions === 'string' + ? { loginHint: loginHintOrOptions } + : loginHintOrOptions ?? {}; + + const { loginHint, themeOptions, localizations } = options; + + // Omitted entirely when unset, so the native side can skip the work and + // behaviour is unchanged for callers that don't customize. + const customization = + themeOptions || localizations ? { themeOptions, localizations } : undefined; + // 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 { - return await FronteggRN.login(loginHint); + return await FronteggRN.login(loginHint, customization); } catch (e) { throw normalizeLoginError(e); } diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index be87679..6159f3e 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -67,10 +67,65 @@ describe('login', () => { (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); await expect(login('hint@example.com')).resolves.toBe('Success'); expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( - 'hint@example.com' + 'hint@example.com', + undefined + ); + }); + + it('accepts no arguments', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + await login(); + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + undefined, + undefined ); }); + // Issue #127: login-box theme/copy overrides. + it('accepts an options object with a login hint', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + await login({ loginHint: 'hint@example.com' }); + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + 'hint@example.com', + undefined + ); + }); + + it('forwards themeOptions and localizations as a customization payload', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + const themeOptions = { + loginBox: { palette: { primary: { main: '#3F6655' } } }, + }; + const localizations = { en: { loginBox: { login: { title: 'Sign-in' } } } }; + + await login({ loginHint: 'hint@example.com', themeOptions, localizations }); + + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + 'hint@example.com', + { themeOptions, localizations } + ); + }); + + it('forwards a partial customization without inventing the other key', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + const themeOptions = { loginBox: { themeName: 'modern' } }; + + await login({ themeOptions }); + + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined, { + themeOptions, + localizations: undefined, + }); + }); + + it('omits the customization payload entirely when nothing is customized', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + await login({ loginHint: 'hint@example.com' }); + const [, customization] = (NativeModules.FronteggRN.login as jest.Mock).mock + .calls[0]; + expect(customization).toBeUndefined(); + }); + it('rejects with a normalized FronteggLoginError carrying the native details', async () => { const nativeError = Object.assign(new Error('Operation canceled by user'), { code: 'user_cancelled', From 7acd0be91f659364baa3f930317432a14684cebe Mon Sep 17 00:00:00 2001 From: Adam Rowe Date: Tue, 1 Sep 2026 13:42:58 -0400 Subject: [PATCH 2/5] feat(android): forward login box overrides to the native SDK Now that frontegg-android-kotlin#286 adds the storage properties, the Android bridge applies the customization instead of warning and ignoring it, so the behaviour matches iOS. --- .../frontegg/reactnative/FronteggRNModule.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index 616e675..c654e68 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -1,7 +1,6 @@ package com.frontegg.reactnative import android.app.Activity -import android.util.Log import android.os.Handler import android.os.Looper import com.facebook.react.bridge.Arguments @@ -18,6 +17,7 @@ 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.services.FronteggInnerStorage import java.io.IOException import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable @@ -151,16 +151,13 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } /** - * @param customization issue #127 login-box theme/copy overrides. Accepted so the - * JS API is identical across platforms, but not yet applied: the Android SDK has - * no equivalent of the iOS `FronteggApp.loginBoxThemeOptions`. Wiring it needs a - * matching change in frontegg-android-kotlin's EmbeddedAuthActivity. + * @param customization issue #127 login-box theme/copy overrides, forwarded to the + * native SDK and deep-merged over the environment's login-box configuration. + * Requires frontegg-android-kotlin#286. */ @ReactMethod fun login(loginHint: String?, customization: ReadableMap?, promise: Promise) { - if (customization != null) { - Log.w("FronteggRN", "loginBox customization is not yet supported on Android; ignoring") - } + applyLoginBoxCustomization(customization) withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> auth.login(activity, loginHint) { error -> resolveOrRejectLogin(error, promise) @@ -168,6 +165,19 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } } + /** + * Forwards issue #127 login-box overrides to the native SDK. Only assigns when a key is + * present, so a caller passing just `themeOptions` does not clear previously set + * localizations. + */ + private fun applyLoginBoxCustomization(customization: ReadableMap?) { + if (customization == null) return + + val storage = FronteggInnerStorage() + customization.getMap("themeOptions")?.let { storage.loginBoxThemeOptions = it.toHashMap() } + customization.getMap("localizations")?.let { storage.loginBoxLocalizations = it.toHashMap() } + } + @ReactMethod fun switchTenant(tenantId: String, promise: Promise) { auth.switchTenant(tenantId) { success -> From 14dd5df0936483837812c0209989cbf1e59658ea Mon Sep 17 00:00:00 2001 From: Adam Rowe Date: Tue, 1 Sep 2026 14:25:00 -0400 Subject: [PATCH 3/5] fix: allow clearing login box overrides, and apply them only once login proceeds Review follow-ups: - There was no way to reset an override. Both native sides assigned only when a key was present and JS omitted the payload when unset, so a previously applied theme persisted for the life of the process. Present-vs-absent is now meaningful: an explicit null clears the override back to the environment's configuration, an absent key leaves it untouched. - The Android bridge mutated shared state before withActivityOrReject, so a rejected login (no current activity) still left the overrides installed for whatever opened a WebView next. Moved inside the guard. - Documented LoginOptions in docs/api.md, which still showed the old signature. - Renamed a test that asserted the opposite of its name, and added coverage for the null-reset contract. Tests: 14 passed. --- .../frontegg/reactnative/FronteggRNModule.kt | 16 ++++++-- docs/api.md | 38 ++++++++++++++++++- ios/FronteggRN.swift | 16 ++++---- src/FronteggNative.ts | 29 ++++++++++---- src/__tests__/index.test.tsx | 14 ++++++- 5 files changed, 91 insertions(+), 22 deletions(-) diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index c654e68..360ddcf 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -157,8 +157,10 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : */ @ReactMethod fun login(loginHint: String?, customization: ReadableMap?, promise: Promise) { - applyLoginBoxCustomization(customization) withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> + // Inside the guard: with no activity the promise rejects and no WebView opens, so + // the overrides must not be left installed for whatever opens one next. + applyLoginBoxCustomization(customization) auth.login(activity, loginHint) { error -> resolveOrRejectLogin(error, promise) } @@ -168,14 +170,20 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : /** * Forwards issue #127 login-box overrides to the native SDK. Only assigns when a key is * present, so a caller passing just `themeOptions` does not clear previously set - * localizations. + * localizations; passing an explicit null clears that key. */ private fun applyLoginBoxCustomization(customization: ReadableMap?) { if (customization == null) return val storage = FronteggInnerStorage() - customization.getMap("themeOptions")?.let { storage.loginBoxThemeOptions = it.toHashMap() } - customization.getMap("localizations")?.let { storage.loginBoxLocalizations = it.toHashMap() } + // Present-vs-absent is meaningful: an explicit null clears the override, an absent + // key leaves whatever was set before. + if (customization.hasKey("themeOptions")) { + storage.loginBoxThemeOptions = customization.getMap("themeOptions")?.toHashMap() + } + if (customization.hasKey("localizations")) { + storage.loginBoxLocalizations = customization.getMap("localizations")?.toHashMap() + } } @ReactMethod diff --git a/docs/api.md b/docs/api.md index 4844d31..c8cfc29 100644 --- a/docs/api.md +++ b/docs/api.md @@ -37,7 +37,7 @@ const { isAuthenticated, user, isLoading } = useAuth(); | Method | Parameters | Returns | Description | |--------|------------|---------|-------------| -| `login` | `loginHint?: string` | `Promise` | Opens the login flow. `loginHint` pre-fills the identifier field. Resolves when login completes; rejects with a `FronteggLoginError`. | +| `login` | `loginHint?: string`
or `options?: LoginOptions` | `Promise` | 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` | Signs the user out and clears stored credentials. | | `loginWithPasskeys` | None | `Promise` | Signs in with a passkey. Needs iOS 15+ or Android API 26+. | | `registerPasskeys` | None | `Promise` | Registers a passkey for the signed-in user. | @@ -54,6 +54,42 @@ 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 \| null` | Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`. | +| `localizations` | `Record \| null` | Same shape as `localizations` from the same endpoint. | + +Values are deep-merged over the environment's configuration, so keys left unset keep +whatever the environment defines. Pass `null` to clear a previously set override; omit a +key to leave it unchanged. + +Embedded mode only — hosted mode runs outside the app's WebView, so there is no +injection point. + ## Step-up authentication | Method | Parameters | Returns | Description | diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index 341d2b2..da31a27 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -213,17 +213,19 @@ class FronteggRN: RCTEventEmitter { } } - /// Forwards issue #127 login-box overrides to the native SDK. Only assigns - /// when a key is present, so a caller passing just `themeOptions` does not - /// clear previously set localizations. + /// Forwards issue #127 login-box overrides to the native SDK. Only assigns when a + /// key is present, so a caller passing just `themeOptions` does not clear previously + /// set localizations; passing an explicit null clears that key. private static func applyLoginBoxCustomization(_ customization: NSDictionary?) { guard let customization else { return } - if let themeOptions = customization["themeOptions"] as? [String: Any] { - FronteggApp.shared.loginBoxThemeOptions = themeOptions + // Present-vs-absent is meaningful: NSNull clears the override, an absent key + // leaves whatever was set before. + if customization.object(forKey: "themeOptions") != nil { + FronteggApp.shared.loginBoxThemeOptions = customization["themeOptions"] as? [String: Any] } - if let localizations = customization["localizations"] as? [String: Any] { - FronteggApp.shared.loginBoxLocalizations = localizations + if customization.object(forKey: "localizations") != nil { + FronteggApp.shared.loginBoxLocalizations = customization["localizations"] as? [String: Any] } } diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index 10d9bc3..2e6fa9c 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -129,6 +129,9 @@ export function normalizeLoginError(e: unknown): FronteggLoginError { * 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 { @@ -136,12 +139,12 @@ export interface LoginBoxCustomization { * Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`, * e.g. `{ loginBox: { palette: { primary: { main: '#3F6655' } } } }`. */ - themeOptions?: Record; + themeOptions?: Record | null; /** * Same shape as `localizations` from `/frontegg/metadata?entityName=adminBox`, * e.g. `{ en: { loginBox: { login: { title: 'Sign-in' } } } }`. */ - localizations?: Record; + localizations?: Record | null; } /** Options accepted by {@link login}. */ @@ -173,18 +176,28 @@ export async function login( ? { loginHint: loginHintOrOptions } : loginHintOrOptions ?? {}; - const { loginHint, themeOptions, localizations } = options; + const { loginHint } = options; - // Omitted entirely when unset, so the native side can skip the work and - // behaviour is unchanged for callers that don't customize. - const customization = - themeOptions || localizations ? { themeOptions, localizations } : undefined; + // Present-vs-absent is meaningful: a key that is present (including `null`) + // is applied, and `null` clears a previously set override back to the + // environment's own configuration. A key that is absent leaves it untouched. + // The whole payload is omitted when neither key is given, so behaviour is + // unchanged for callers that don't customize. + const customization: Record = {}; + if ('themeOptions' in options) { + customization.themeOptions = options.themeOptions ?? null; + } + if ('localizations' in options) { + customization.localizations = options.localizations ?? null; + } + const payload = + Object.keys(customization).length > 0 ? customization : undefined; // 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 { - return await FronteggRN.login(loginHint, customization); + return await FronteggRN.login(loginHint, payload); } catch (e) { throw normalizeLoginError(e); } diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 6159f3e..47d2925 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -106,7 +106,7 @@ describe('login', () => { ); }); - it('forwards a partial customization without inventing the other key', async () => { + it('forwards only the key that was provided', async () => { (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); const themeOptions = { loginBox: { themeName: 'modern' } }; @@ -114,7 +114,17 @@ describe('login', () => { expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined, { themeOptions, - localizations: undefined, + }); + }); + + it('treats an explicit null as a reset rather than an omission', async () => { + (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + + await login({ themeOptions: null }); + + // Present-with-null clears the override; absent would leave it untouched. + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined, { + themeOptions: null, }); }); From de3a679846eeb15e6d6ed1e35bb54e3263f65981 Mon Sep 17 00:00:00 2001 From: dianaKhortiuk-frontegg Date: Tue, 8 Sep 2026 10:00:08 +0100 Subject: [PATCH 4/5] fix: make login box overrides per-call and keep login() callable on older natives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Overrides live in process-global native state, and the previous "omit a key to leave it unchanged" rule meant a later login() with no options silently reused the previous brand's theme — brand A's logo on brand B's login screen, visible only from the second login onward. Each call now fully determines appearance: an omitted field is sent as null and clears. That also removes the trap where `{ themeOptions: brand?.theme }` with an unresolved brand read as "keep whatever was set" instead of "no theme". Adding a parameter to the existing login bridge method meant a JS-only update (CodePush/Expo) against an older native binary failed argument-count validation and broke sign-in entirely. Keep login() as it was and add loginWithOptions, falling back when it is absent so the theming degrades, not the login. Expose isLoginBoxCustomizationSupported so a white-label host can detect an Android WebView provider that cannot apply overrides instead of shipping the wrong brand. Co-Authored-By: Claude Opus 5 --- .../frontegg/reactnative/FronteggRNModule.kt | 43 ++++--- docs/api.md | 27 ++++- ios/FronteggRN.m | 9 ++ ios/FronteggRN.swift | 49 +++++--- src/FronteggNative.ts | 48 +++++--- src/__tests__/index.test.tsx | 110 +++++++++--------- src/index.tsx | 3 + 7 files changed, 183 insertions(+), 106 deletions(-) diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index 360ddcf..10d09d1 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -17,6 +17,7 @@ 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 @@ -150,16 +151,23 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } + @ReactMethod + fun login(loginHint: String?, promise: Promise) { + withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> + auth.login(activity, loginHint) { error -> + resolveOrRejectLogin(error, promise) + } + } + } + /** - * @param customization issue #127 login-box theme/copy overrides, forwarded to the - * native SDK and deep-merged over the environment's login-box configuration. - * Requires frontegg-android-kotlin#286. + * 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 login(loginHint: String?, customization: ReadableMap?, promise: Promise) { + fun loginWithOptions(loginHint: String?, customization: ReadableMap?, promise: Promise) { withActivityOrReject(reactApplicationContext.currentActivity, promise) { activity -> - // Inside the guard: with no activity the promise rejects and no WebView opens, so - // the overrides must not be left installed for whatever opens one next. applyLoginBoxCustomization(customization) auth.login(activity, loginHint) { error -> resolveOrRejectLogin(error, promise) @@ -167,23 +175,20 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : } } + @ReactMethod + fun isLoginBoxCustomizationSupported(promise: Promise) { + promise.resolve(LoginBoxCustomization.isSupported()) + } + /** - * Forwards issue #127 login-box overrides to the native SDK. Only assigns when a key is - * present, so a caller passing just `themeOptions` does not clear previously set - * localizations; passing an explicit null clears that key. + * 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?) { - if (customization == null) return - val storage = FronteggInnerStorage() - // Present-vs-absent is meaningful: an explicit null clears the override, an absent - // key leaves whatever was set before. - if (customization.hasKey("themeOptions")) { - storage.loginBoxThemeOptions = customization.getMap("themeOptions")?.toHashMap() - } - if (customization.hasKey("localizations")) { - storage.loginBoxLocalizations = customization.getMap("localizations")?.toHashMap() - } + storage.loginBoxThemeOptions = customization?.getMap("themeOptions")?.toHashMap() + storage.loginBoxLocalizations = customization?.getMap("localizations")?.toHashMap() } @ReactMethod diff --git a/docs/api.md b/docs/api.md index c8cfc29..d385386 100644 --- a/docs/api.md +++ b/docs/api.md @@ -80,16 +80,33 @@ await login({ | Field | Type | Description | |-------|------|-------------| | `loginHint` | `string` | Pre-fills the identifier field. | -| `themeOptions` | `Record \| null` | Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`. | -| `localizations` | `Record \| null` | Same shape as `localizations` from the same endpoint. | +| `themeOptions` | `Record` | Same shape as `themeV2` from `/frontegg/metadata?entityName=adminBox`. Omitted means cleared. | +| `localizations` | `Record` | Same shape as `localizations` from the same endpoint. Omitted means cleared. | -Values are deep-merged over the environment's configuration, so keys left unset keep -whatever the environment defines. Pass `null` to clear a previously set override; omit a -key to leave it unchanged. +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` | 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 | diff --git a/ios/FronteggRN.m b/ios/FronteggRN.m index 1de8638..33a06a3 100644 --- a/ios/FronteggRN.m +++ b/ios/FronteggRN.m @@ -11,10 +11,19 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter) ) RCT_EXTERN_METHOD( login: (NSString *)loginHint + 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 diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index da31a27..fefe374 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -177,13 +177,42 @@ class FronteggRN: RCTEventEmitter { @objc func login( + _ 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 { - Self.applyLoginBoxCustomization(customization) + if applyCustomization { + Self.applyLoginBoxCustomization(customization) + } let completion: FronteggAuth.CompletionHandler = { result in switch(result) { case .success(_): @@ -213,20 +242,12 @@ class FronteggRN: RCTEventEmitter { } } - /// Forwards issue #127 login-box overrides to the native SDK. Only assigns when a - /// key is present, so a caller passing just `themeOptions` does not clear previously - /// set localizations; passing an explicit null clears that key. + /// 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?) { - guard let customization else { return } - - // Present-vs-absent is meaningful: NSNull clears the override, an absent key - // leaves whatever was set before. - if customization.object(forKey: "themeOptions") != nil { - FronteggApp.shared.loginBoxThemeOptions = customization["themeOptions"] as? [String: Any] - } - if customization.object(forKey: "localizations") != nil { - FronteggApp.shared.loginBoxLocalizations = customization["localizations"] as? [String: Any] - } + 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 diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index 2e6fa9c..62c068f 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -178,31 +178,49 @@ export async function login( const { loginHint } = options; - // Present-vs-absent is meaningful: a key that is present (including `null`) - // is applied, and `null` clears a previously set override back to the - // environment's own configuration. A key that is absent leaves it untouched. - // The whole payload is omitted when neither key is given, so behaviour is - // unchanged for callers that don't customize. - const customization: Record = {}; - if ('themeOptions' in options) { - customization.themeOptions = options.themeOptions ?? null; - } - if ('localizations' in options) { - customization.localizations = options.localizations ?? null; - } - const payload = - Object.keys(customization).length > 0 ? customization : undefined; + // 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 { - return await FronteggRN.login(loginHint, payload); + 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 { + if (typeof FronteggRN.isLoginBoxCustomizationSupported !== 'function') { + return false; + } + return FronteggRN.isLoginBoxCustomizationSupported(); +} + export function logout(): Promise { return FronteggRN.logout(); } diff --git a/src/__tests__/index.test.tsx b/src/__tests__/index.test.tsx index 47d2925..a5586aa 100644 --- a/src/__tests__/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -59,40 +59,40 @@ describe('directLoginAction', () => { // Issue #110: login() rejects with a typed, cross-platform FronteggLoginError. describe('login', () => { + const NONE = { themeOptions: null, localizations: null }; + beforeEach(() => { - (NativeModules.FronteggRN.login as jest.Mock).mockReset(); + (NativeModules.FronteggRN.login as jest.Mock).mockClear(); + NativeModules.FronteggRN.loginWithOptions = jest.fn(() => + Promise.resolve('Success') + ); }); - it('resolves when the native login succeeds', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + it('bridges a login hint', async () => { await expect(login('hint@example.com')).resolves.toBe('Success'); - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith( 'hint@example.com', - undefined + NONE ); }); it('accepts no arguments', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); await login(); - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith( undefined, - undefined + NONE ); }); - // Issue #127: login-box theme/copy overrides. it('accepts an options object with a login hint', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); await login({ loginHint: 'hint@example.com' }); - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith( 'hint@example.com', - undefined + NONE ); }); - it('forwards themeOptions and localizations as a customization payload', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + it('forwards themeOptions and localizations', async () => { const themeOptions = { loginBox: { palette: { primary: { main: '#3F6655' } } }, }; @@ -100,61 +100,65 @@ describe('login', () => { await login({ loginHint: 'hint@example.com', themeOptions, localizations }); - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith( 'hint@example.com', { themeOptions, localizations } ); }); - it('forwards only the key that was provided', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); - const themeOptions = { loginBox: { themeName: 'modern' } }; + // A key the caller omitted must be cleared, not inherited, so the previous brand's + // theme cannot render on the next brand's login. + it('clears an override the caller did not supply', async () => { + const themeOptions = { + loginBox: { logo: { image: 'https://a/logo.png' } }, + }; await login({ themeOptions }); + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenLastCalledWith( + undefined, + { themeOptions, localizations: null } + ); - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined, { - themeOptions, - }); + await login(); + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenLastCalledWith( + undefined, + NONE + ); }); - it('treats an explicit null as a reset rather than an omission', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); + // `{ themeOptions: brand?.theme }` with an unresolved brand must not be read as an + // instruction to keep whatever was set before. + it('treats an explicitly undefined override as cleared', async () => { + await login({ loginHint: 'hint@example.com', themeOptions: undefined }); + expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith( + 'hint@example.com', + NONE + ); + }); - await login({ themeOptions: null }); + describe('against a native binary without loginWithOptions', () => { + beforeEach(() => { + delete (NativeModules.FronteggRN as Record) + .loginWithOptions; + }); - // Present-with-null clears the override; absent would leave it untouched. - expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined, { - themeOptions: null, + it('still signs in, using the legacy method', async () => { + await expect(login('hint@example.com')).resolves.toBe('Success'); + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith( + 'hint@example.com' + ); }); - }); - it('omits the customization payload entirely when nothing is customized', async () => { - (NativeModules.FronteggRN.login as jest.Mock).mockResolvedValue('Success'); - await login({ loginHint: 'hint@example.com' }); - const [, customization] = (NativeModules.FronteggRN.login as jest.Mock).mock - .calls[0]; - expect(customization).toBeUndefined(); - }); + it('warns when overrides were requested but cannot be applied', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); - it('rejects with a normalized FronteggLoginError carrying the native details', async () => { - const nativeError = Object.assign(new Error('Operation canceled by user'), { - code: 'user_cancelled', - userInfo: { - nativeCode: 'operationCanceled', - nativeMessage: 'Operation canceled by user', - }, - }); - (NativeModules.FronteggRN.login as jest.Mock).mockRejectedValue( - nativeError - ); + await login({ themeOptions: { loginBox: {} } }); - await expect(login()).rejects.toMatchObject({ - name: 'FronteggLoginError', - code: 'user_cancelled', - userCancelled: true, - message: 'Operation canceled by user', - nativeCode: 'operationCanceled', - nativeMessage: 'Operation canceled by user', + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('needs a newer native SDK') + ); + expect(NativeModules.FronteggRN.login).toHaveBeenCalledWith(undefined); + warn.mockRestore(); }); }); }); diff --git a/src/index.tsx b/src/index.tsx index 5eaf420..f21850f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,10 +17,13 @@ export { loadEntitlements, getFeatureEntitlement, getPermissionEntitlement, + isLoginBoxCustomizationSupported, } from './FronteggNative'; export type { Entitlement, FronteggConstants, FronteggLoginError, FronteggLoginErrorCode, + LoginBoxCustomization, + LoginOptions, } from './FronteggNative'; From f615654b381068aaa6c79bd5c2057ce4f5a41afc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:01:36 +0000 Subject: [PATCH 5/5] FR-26903 - chore: update native SDKs to Android 1.3.41 / iOS 1.3.21 Picks up the embedded login box runtime theme and copy overrides, and the fix for the SDK retrying forever on a rejected refresh token. Bumps every pin, not only the plugin's: the example apps tracked older versions than the plugin did, and the SPM lockfiles still resolved 1.3.10. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ALKJ6thT3mssEYmEsmR5r2 --- FronteggRN.podspec | 2 +- android/build.gradle | 2 +- docs/advanced.md | 2 +- docs/setup.md | 2 +- example/android/app/build.gradle | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 ++-- ios/Package.swift | 2 +- ios/frontegg_spm.rb | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/FronteggRN.podspec b/FronteggRN.podspec index 866099d..5706346 100644 --- a/FronteggRN.podspec +++ b/FronteggRN.podspec @@ -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 diff --git a/android/build.gradle b/android/build.gradle index 7cc6108..ee8e34c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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" } diff --git a/docs/advanced.md b/docs/advanced.md index d11be3b..d97a90d 100644 --- a/docs/advanced.md +++ b/docs/advanced.md @@ -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' } ``` diff --git a/docs/setup.md b/docs/setup.md index 6d9d0fd..0149b14 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -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 `embeddedMode` in `Frontegg.plist` if you use the embedded login box. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 8a5f8d6..cfb57c0 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -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' diff --git a/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved index 12df1f6..e7b6cbc 100644 --- a/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/example/ios/ReactNativeExample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/frontegg/frontegg-ios-swift.git", "state" : { - "revision" : "e702dd7d4fedd1290cee9ca9a72b0b8ed24a2fd3", - "version" : "1.3.10" + "revision" : "12f35530c8bc0ac3ecb100285cedefc3c1879f18", + "version" : "1.3.21" } }, { diff --git a/ios/Package.swift b/ios/Package.swift index 45477c0..1f2881c 100644 --- a/ios/Package.swift +++ b/ios/Package.swift @@ -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: [] ) diff --git a/ios/frontegg_spm.rb b/ios/frontegg_spm.rb index 6aa41c6..3317bb5 100644 --- a/ios/frontegg_spm.rb +++ b/ios/frontegg_spm.rb @@ -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'