Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) :
val storage = FronteggInnerStorage()
storage.loginBoxThemeOptions = customization?.getMap("themeOptions")?.toHashMap()
storage.loginBoxLocalizations = customization?.getMap("localizations")?.toHashMap()
storage.loginBoxFooter = customization?.getMap("footer")?.toHashMap()
}

@ReactMethod
Expand Down
41 changes: 41 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ await login({
| `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. |
| `footer` | `LoginBoxFooter` | Content below the login box's card, on its login screen only. See [Login box footer](#login-box-footer). Omitted means cleared. |

Values are deep-merged over the environment's configuration, so keys the override does not
mention keep whatever the environment defines.
Expand All @@ -107,6 +108,46 @@ if (!(await isLoginBoxCustomizationSupported())) {
|--------|------------|---------|-------------|
| `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. |

### Login box footer

The login box's configuration has no slot below the card, which `@frontegg/react` covers with
its `boxFooter` render prop. `footer` is the embedded-mode equivalent, built from a structured
payload rather than markup:

```ts
await login({
footer: {
hideCaptchaBadge: true,
rows: [
{
variant: 'body',
segments: [
{ text: "Don't have an account? " },
{ label: 'Sign up now', url: 'myapp://sign-up' },
],
},
{
variant: 'fine',
segments: [
{ text: 'Protected by reCAPTCHA — ' },
{ label: 'Privacy Policy', url: 'https://policies.google.com/privacy' },
],
},
],
},
});
```

- Strings are always rendered as text, never parsed as markup.
- `variant` is `'body'` or `'fine'` (small, de-emphasised legal text).
- Link URLs must be absolute `http(s)` or use one of the app's own registered URL schemes;
anything else renders as plain text.
- `http(s)` links open in the system browser and leave the box in place. A link on the app's
own scheme dismisses the box and hands the URL to the app.
- `hideCaptchaBadge` hides Google's floating reCAPTCHA badge. Google permits this only when the
attribution is shown instead, so set it together with a row naming reCAPTCHA and linking
Google's Privacy Policy and Terms.

## Step-up authentication

| Method | Parameters | Returns | Description |
Expand Down
1 change: 1 addition & 0 deletions ios/FronteggRN.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ class FronteggRN: RCTEventEmitter {
private static func applyLoginBoxCustomization(_ customization: NSDictionary?) {
FronteggApp.shared.loginBoxThemeOptions = customization?["themeOptions"] as? [String: Any]
FronteggApp.shared.loginBoxLocalizations = customization?["localizations"] as? [String: Any]
FronteggApp.shared.loginBoxFooter = customization?["footer"] as? [String: Any]
}

/// Maps a FronteggError from login() onto the stable, cross-platform rejection codes
Expand Down
43 changes: 42 additions & 1 deletion src/FronteggNative.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,44 @@ export interface LoginBoxCustomization {
* e.g. `{ en: { loginBox: { login: { title: 'Sign-in' } } } }`.
*/
localizations?: Record<string, unknown> | null;
/**
* Content appended below the login box's card, on its login screen only.
*
* The React SDK exposes a `boxFooter` render prop for this; a box served into
* a WebView has no equivalent, and the box's own configuration has no slot
* for content below the card. Structured rather than HTML — strings are
* always rendered as text, never parsed as markup.
*
* Link URLs must be absolute `http(s)` or use one of the app's own
* registered URL schemes; anything else renders as plain text. `http(s)`
* links are handed to the OS rather than loaded in the box, which has no
* navigation chrome — an app-scheme link instead dismisses the box and hands
* off to the app.
*/
footer?: LoginBoxFooter | null;
}

/** A run of footer content: literal text, or a link. */
export type LoginBoxFooterSegment =
| { text: string }
| { label: string; url: string };

/** One centred line of footer content. */
export interface LoginBoxFooterRow {
/** `'fine'` is small, de-emphasised legal text. Defaults to `'body'`. */
variant?: 'body' | 'fine';
segments: LoginBoxFooterSegment[];
}

/** See {@link LoginBoxCustomization.footer}. */
export interface LoginBoxFooter {
/**
* Hides Google's floating reCAPTCHA badge. Google's terms permit this only
* when the attribution it carries is shown instead, so set it together with
* a row naming reCAPTCHA and linking Google's Privacy Policy and Terms.
*/
hideCaptchaBadge?: boolean;
rows: LoginBoxFooterRow[];
}

/** Options accepted by {@link login}. */
Expand Down Expand Up @@ -184,9 +222,12 @@ export async function login(
const customization = {
themeOptions: options.themeOptions ?? null,
localizations: options.localizations ?? null,
footer: options.footer ?? null,
};
const customized =
customization.themeOptions !== null || customization.localizations !== null;
customization.themeOptions !== null ||
customization.localizations !== null ||
customization.footer !== 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
Expand Down
31 changes: 28 additions & 3 deletions src/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('directLoginAction', () => {

// Issue #110: login() rejects with a typed, cross-platform FronteggLoginError.
describe('login', () => {
const NONE = { themeOptions: null, localizations: null };
const NONE = { themeOptions: null, localizations: null, footer: null };

beforeEach(() => {
(NativeModules.FronteggRN.login as jest.Mock).mockClear();
Expand Down Expand Up @@ -102,7 +102,32 @@ describe('login', () => {

expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith(
'hint@example.com',
{ themeOptions, localizations }
{ themeOptions, localizations, footer: null }
);
});

it('forwards a footer', async () => {
const footer = {
hideCaptchaBadge: true,
rows: [
{
variant: 'fine' as const,
segments: [
{ text: 'Protected by reCAPTCHA — ' },
{
label: 'Privacy Policy',
url: 'https://policies.google.com/privacy',
},
],
},
],
};

await login({ footer });

expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenCalledWith(
undefined,
{ themeOptions: null, localizations: null, footer }
);
});

Expand All @@ -116,7 +141,7 @@ describe('login', () => {
await login({ themeOptions });
expect(NativeModules.FronteggRN.loginWithOptions).toHaveBeenLastCalledWith(
undefined,
{ themeOptions, localizations: null }
{ themeOptions, localizations: null, footer: null }
);

await login();
Expand Down
Loading