feat: accept login box theme and localization overrides in login() - #128
Merged
dianaKhortiuk-frontegg merged 5 commits intoSep 17, 2026
Conversation
Closes frontegg#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.
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.
…in 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.
airowe
marked this pull request as ready for review
September 1, 2026 20:33
…lder natives
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 <noreply@anthropic.com>
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ALKJ6thT3mssEYmEsmR5r2
dianaKhortiuk-frontegg
approved these changes
Sep 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #127 (JS side).
Review the native PRs first — they carry the mechanism and this one only forwards to them:
This PR does not build until those land, since it references properties they add. Android CI currently fails with
Unresolved reference: LoginBoxCustomization/loginBoxThemeOptions/loginBoxLocalizationsinFronteggRNModule.kt— that is expected, not a defect in this diff, and will clear when #286 merges.What
login()now accepts either a login hint (unchanged) or aLoginOptionsobject:themeOptionsandlocalizationsuse the same shapes asthemeV2andlocalizationsfrom/frontegg/metadata?entityName=adminBox, and are deep-merged over the environment's configuration — keys left unset keep whatever the environment defines.Why
Login-box configuration is scoped to a Frontegg environment:
POST /metadatais keyed byvendorId, with noapplicationIdfield or header, and the Builder has no application selector. That's fine for static branding.It doesn't work when appearance is only known at runtime. We ship many white-labeled apps from one codebase and resolve each brand's logo and colors from our own backend. On web we pass exactly these two objects to
FronteggProvideras props — fully programmatic, no per-brand resources created in Frontegg.How it reaches the login box
The native SDKs set
window.__fronteggLoginBoxOverridesat document start; the login box reads that global and deep-merges it over its own configuration. That contract shipped in frontegg/oauth-service#860.Two things worth review attention
Per-call semantics. Overrides live in process-global native state. An earlier revision used "omit a key to leave it unchanged", which 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 asnulland clears. That also removes the trap where{ themeOptions: brand?.theme }with an unresolved brand read as "keep whatever was set" instead of "no theme".loginWithOptionsrather than a new parameter onlogin. Adding a parameter to the existing bridge method meant a JS-only update (CodePush/Expo) against an older native binary failed argument-count validation and broke sign-in entirely.login()is unchanged on the native side; JS callsloginWithOptionswhen present and falls back otherwise, warning once — so the theming degrades, not the login.Platform status
FronteggApp.loginBoxThemeOptions/.loginBoxLocalizations. Requires frontegg/frontegg-ios-swift#320.FronteggInnerStorage.loginBoxThemeOptions/.loginBoxLocalizations. Requires frontegg/frontegg-android-kotlin#286 — this will not compile until that lands.isLoginBoxCustomizationSupported()is exposed so a white-label host can detect an Android WebView provider that cannot apply overrides, instead of shipping the wrong brand. Alwaystrueon iOS;falseon native binaries older than this feature.Compatibility
The string form of
login()is untouched. Existing callers reachloginWithOptions(hint, { themeOptions: null, localizations: null }), which applies no customization.Tests
src/__tests__/index.test.tsxcovers the string form, the options form, no arguments, forwarding of both fields, the clear-on-omit contract, an explicitlyundefinedoverride, and the fallback path against a native binary withoutloginWithOptions(both that sign-in still works and that it warns).docs/api.mddocumentsLoginOptionsandisLoginBoxCustomizationSupported.The JS suite passes locally; CI cannot go green until the native PRs land, for the compile reason above.
Shipping order
None of this should ship before oauth-service#860 is deployed: until then nothing reads the global, and the box renders the environment's branding.
Open on the API shape — an options object on
login()seemed least disruptive, but init-time configuration would work equally well for us, and would additionally coverdirectLoginAction/stepUp/registerPasskeys, which open the same WebView.Note
Medium Risk
Changes the login entry path and depends on new native SDK APIs; backward-compatible fallbacks limit breakage, but incorrect override clearing or platform support checks could affect white-label UX.
Overview
Adds runtime embedded login box branding for multi-brand apps:
login()now accepts aLoginOptionsobject (loginHint,themeOptions,localizations) in addition to the existing string hint, and exportsisLoginBoxCustomizationSupported()for Android WebViews that cannot apply overrides.JS routes customization through a new native bridge method
loginWithOptions(iOS/Android) while keeping the originalloginsignature unchanged; older native binaries still sign in vialogin()with a console warning if overrides were requested. Each call sends omitted override fields asnullso a previous brand’s theme does not leak into the next login.Native layers forward overrides into the upstream SDKs (
FronteggApp/FronteggInnerStorage). FronteggSwift is pinned to 1.3.21 and Androidcom.frontegg.sdk:androidto 1.3.41. Docs and tests cover the new API and fallback behavior.Reviewed by Cursor Bugbot for commit f615654. Bugbot is set up for automated code reviews on this repo. Configure here.