Skip to content

feat: accept login box theme and localization overrides in login() - #128

Merged
dianaKhortiuk-frontegg merged 5 commits into
frontegg:masterfrom
airowe:feat/login-box-theme-and-localization-overrides
Sep 17, 2026
Merged

dianaKhortiuk-frontegg merged 5 commits into
frontegg:masterfrom
airowe:feat/login-box-theme-and-localization-overrides

Conversation

@airowe

@airowe airowe commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

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 / loginBoxLocalizations in FronteggRNModule.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 a LoginOptions object:

await login('user@example.com');                     // unchanged

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' } } } },
});

themeOptions and localizations use the same shapes as themeV2 and localizations from /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 /metadata is keyed by vendorId, with no applicationId field 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 FronteggProvider as props — fully programmatic, no per-brand resources created in Frontegg.

How it reaches the login box

The native SDKs set window.__fronteggLoginBoxOverrides at document start; the login box reads that global and deep-merges it over its own configuration. That contract shipped in frontegg/oauth-service#860.

Note on an earlier revision. The native PRs originally wrapped window.fetch to rewrite the /frontegg/metadata response in flight. They no longer do. Nothing in this stack intercepts network traffic any more.

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 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".

loginWithOptions rather than a new parameter on login. 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 calls loginWithOptions when present and falls back otherwise, warning once — so the theming degrades, not the login.

Platform status

iOS Forwards to FronteggApp.loginBoxThemeOptions / .loginBoxLocalizations. Requires frontegg/frontegg-ios-swift#320.
Android Forwards to FronteggInnerStorage.loginBoxThemeOptions / .loginBoxLocalizations. Requires frontegg/frontegg-android-kotlin#286this 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. Always true on iOS; false on native binaries older than this feature.

Compatibility

The string form of login() is untouched. Existing callers reach loginWithOptions(hint, { themeOptions: null, localizations: null }), which applies no customization.

Tests

src/__tests__/index.test.tsx covers the string form, the options form, no arguments, forwarding of both fields, the clear-on-omit contract, an explicitly undefined override, and the fallback path against a native binary without loginWithOptions (both that sign-in still works and that it warns). docs/api.md documents LoginOptions and isLoginBoxCustomizationSupported.

The JS suite passes locally; CI cannot go green until the native PRs land, for the compile reason above.

Shipping order

  1. frontegg/oauth-service#860 — merged, awaiting deploy.
  2. feat(embedded): runtime theme and localization overrides for the login box frontegg-ios-swift#320 and feat(embedded): runtime theme and localization overrides for the login box frontegg-android-kotlin#286.
  3. This PR.

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 cover directLoginAction / 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 a LoginOptions object (loginHint, themeOptions, localizations) in addition to the existing string hint, and exports isLoginBoxCustomizationSupported() for Android WebViews that cannot apply overrides.

JS routes customization through a new native bridge method loginWithOptions (iOS/Android) while keeping the original login signature unchanged; older native binaries still sign in via login() with a console warning if overrides were requested. Each call sends omitted override fields as null so 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 Android com.frontegg.sdk:android to 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.

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
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
dianaKhortiuk-frontegg merged commit 6167221 into frontegg:master Sep 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: expose themeOptions / localizations for the embedded login box (parity with @frontegg/react)

3 participants