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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ DESCOPE_TENANT_ID= # Your Descope Tenant ID
DESCOPE_FLOW_ID="sign-up-or-in" # Your Descope flow ID
DESCOPE_STYLE_ID= # Your Descope Style ID
DESCOPE_FLOW_DEBUG= # Set to true in case you want to debug your flow
DESCOPE_BG= # Optional page background color or https:// image URL
DESCOPE_FLOW_LOADING= # Set to true to show a loading spinner while the flow initializes
DESCOPE_LOADING_COLOR= # Optional loading spinner color (defaults to #0082b5)
DESCOPE_LOADING_TIMEOUT_MS= # Optional loading overlay timeout in milliseconds (default 15000)
REACT_APP_DESCOPE_BASE_URL= # Descope API base URL
REACT_APP_USE_ORIGIN_BASE_URL= # Set in case you want to use the origin as
REACT_APP_FAVICON_URL= # Set in case you want to use a custom favicon
Expand Down
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,25 @@ These are the different query parameters you can use:
- **Color name**: You can use a [web color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value), e.g. `bg=red`, `bg=%23ff0000`. Note that some symbols such as `#` will have to be URL encoded.
- **Image URL**: You can specify a URL to an image such as `https://example.com/background.png`. This image will be sized to cover the screen.

6. `wide` query parameter is optional. If wide mode is nedded use `wide=true`. This will widen the flow component that is rendered, which is used for large forms made with Flow screens.
6. `loading` query parameter is optional. By default, no loading spinner is shown. Set `loading=true` to show a spinner while the flow initializes and during redirect steps (for example, when the first step is SSO). The overlay automatically dismisses after a timeout if the flow never becomes ready (default 15 seconds). The overlay is tinted with `bg` when `bg` is a color, so dark pages do not flash white; when `bg` is an image URL or unset, the overlay falls back to white.

7. `theme` query parameter is optional. The default value is `light`, but otherwise it will override the theme for your flows rendered with the SDK.
7. `loading_color` query parameter is optional. Sets the loading spinner color using the same formats as `bg` (including bare hex, e.g. `loading_color=ffffff`). Only applies when `loading=true`. If omitted, or if the value is not a valid CSS color (an image URL, for example), the spinner defaults to `#0082b5`.

8. `style` query parameter is optional. The default style in your project will be used if not defined, but this allows you to override the `style` for the flows rendered with the SDK.
8. `loading_timeout` query parameter is optional. Sets how long the loading overlay stays visible before auto-dismiss, in seconds (for example, `loading_timeout=20`). Only applies when `loading=true`.

9. `store_last_auth_user` query parameter is optional. Pass this parameter to ensure the last authenticated user is not saved when the flow ends. For example, append `store_last_auth_user=false` to the URL to disable saving the last user.
9. `wide` query parameter is optional. If wide mode is nedded use `wide=true`. This will widen the flow component that is rendered, which is used for large forms made with Flow screens.

10. Additional query parameters prefixed with `client.` are passed to the `Descope` component as its `client` prop. For example: `client.k1=v1&client.k2=v2` becomes `{ k1: 'v1', k2: 'v2' }`.
10. `theme` query parameter is optional. The default value is `light`, but otherwise it will override the theme for your flows rendered with the SDK.

11. `width` & `height` are optional query parameters, controlling the sizing of the flow screen in either pixels or a percentage of the viewport (e.g. `50%`, `1200px`). Any value larger than the screen is clamped down.
11. `style` query parameter is optional. The default style in your project will be used if not defined, but this allows you to override the `style` for the flows rendered with the SDK.

12. `title` query parameter is optional. If provided, it sets the browser tab/document title (e.g. `title=Sign%20in`).
12. `store_last_auth_user` query parameter is optional. Pass this parameter to ensure the last authenticated user is not saved when the flow ends. For example, append `store_last_auth_user=false` to the URL to disable saving the last user.

13. Additional query parameters prefixed with `client.` are passed to the `Descope` component as its `client` prop. For example: `client.k1=v1&client.k2=v2` becomes `{ k1: 'v1', k2: 'v2' }`.

14. `width` & `height` are optional query parameters, controlling the sizing of the flow screen in either pixels or a percentage of the viewport (e.g. `50%`, `1200px`). Any value larger than the screen is clamped down.

15. `title` query parameter is optional. If provided, it sets the browser tab/document title (e.g. `title=Sign%20in`).

**Using .env**

Expand Down
46 changes: 46 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,52 @@ body,
display: inline-block;
}

.flow-loading-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
/* color-mix() is not polyfilled by the build and the browserslist targets
still include pre-16.2 iOS Safari, so keep an opaque fallback first.
Without it an unsupported color-mix() drops the declaration entirely and
leaves an invisible layer that still blocks clicks. */
background: var(--flow-loading-overlay-color, #ffffff);
background: color-mix(
in srgb,
var(--flow-loading-overlay-color, #ffffff) 88%,
transparent
);
Comment thread
gaokevin1 marked this conversation as resolved.
backdrop-filter: blur(2px);
}

.flow-loading-spinner {
width: 44px;
height: 44px;
border-radius: 50%;
/* The track is a static color on purpose. Any color-mix() here ends up
inside the border shorthand after minification, and an unsupported
color-mix() then drops border-style to none and hides the spinner
completely. Only the accent arc reads the custom property, so a color
that fails to resolve costs the arc, never the whole indicator. */
border: 3px solid rgba(128, 128, 128, 0.25);
border-top-color: var(--flow-loading-color, #0082b5);
animation: flow-loading-spin 0.8s linear infinite;
Comment thread
gaokevin1 marked this conversation as resolved.
}

@media (prefers-reduced-motion: reduce) {
.flow-loading-spinner {
animation: none;
}
}

@keyframes flow-loading-spin {
to {
transform: rotate(360deg);
}
}

h1 {
margin: 0px;
font-weight: 800;
Expand Down
96 changes: 95 additions & 1 deletion src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@testing-library/jest-dom';
import React, { PropsWithChildren } from 'react';
import {
act,
render,
fireEvent,
screen,
Expand All @@ -15,11 +16,24 @@ import { env } from './env';

const mockDescope = jest.fn();
const mockAuthProvider = jest.fn();
const mockDescopeControls = {
shouldFireOnReady: true
};

jest.mock('@descope/react-sdk', () => ({
...jest.requireActual('@descope/react-sdk'),
Descope: ({ onSuccess, ...props }: { onSuccess: () => void }) => {
Descope: ({
onSuccess,
onReady = () => {},
...props
}: {
onSuccess: () => void;
onReady: () => void;
}) => {
mockDescope(props);
if (mockDescopeControls.shouldFireOnReady) {
setTimeout(onReady, 0);
}
return (
<button data-testid="descope-button" type="button" onClick={onSuccess}>
Descope
Expand Down Expand Up @@ -66,6 +80,7 @@ describe('App component', () => {
jest.resetModules();
mockFetch.mockReset();
mockFetch.mockResolvedValue({ ok: false });
mockDescopeControls.shouldFireOnReady = true;
delete env.REACT_APP_DESCOPE_BASE_URL;
delete env.REACT_APP_USE_ORIGIN_BASE_URL;
env.DESCOPE_PROJECT_ID = '';
Expand Down Expand Up @@ -173,6 +188,67 @@ describe('App component', () => {
);
});

test('shows a loading overlay until the flow is ready when loading=true', async () => {
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}&loading=true`;
render(<App />);
expect(screen.getByTestId('flow-loading-overlay')).toBeInTheDocument();
await waitFor(() =>
expect(
screen.queryByTestId('flow-loading-overlay')
).not.toBeInTheDocument()
);
});

test('hides the loading overlay by default', async () => {
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}`;
render(<App />);
await waitFor(() =>
expect(mockDescope).toHaveBeenCalledWith(
expect.objectContaining({ flowId })
)
);
expect(
screen.queryByTestId('flow-loading-overlay')
).not.toBeInTheDocument();
});

test('uses loading_color for the spinner when provided', async () => {
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}&loading=true&loading_color=ff0000`;
render(<App />);
const overlay = await screen.findByTestId('flow-loading-overlay');
expect(overlay).toHaveStyle({ '--flow-loading-color': '#ff0000' });
});

test('keeps default spinner color when only bg is provided', async () => {
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}&loading=true&bg=ffffff`;
render(<App />);
const overlay = await screen.findByTestId('flow-loading-overlay');
expect(overlay).toHaveStyle({ '--flow-loading-color': '#0082b5' });
expect(overlay).toHaveStyle({ '--flow-loading-overlay-color': '#ffffff' });
});

test('dismisses the loading overlay after loading_timeout when onReady never fires', async () => {
jest.useFakeTimers();
mockDescopeControls.shouldFireOnReady = false;
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}&loading=true&loading_timeout=5`;
render(<App />);
expect(screen.getByTestId('flow-loading-overlay')).toBeInTheDocument();

act(() => {
jest.advanceTimersByTime(5000);
});

expect(
screen.queryByTestId('flow-loading-overlay')
).not.toBeInTheDocument();
jest.useRealTimers();
});

test('that send_session_token search param enables sendSessionToken', async () => {
window.location.pathname = `/${packageJson.homepage}/${validProjectId}`;
window.location.search = `?flow=${flowId}&send_session_token=true`;
Expand Down Expand Up @@ -734,5 +810,23 @@ describe('App component', () => {
);
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
});

// The flow normally has not become ready by the time the domain check
// resolves, so onReady never fires and cannot clear the overlay for us.
it('hides the loading overlay when the domain is not approved', async () => {
mockDescopeControls.shouldFireOnReady = false;
mockFetch.mockResolvedValue({
ok: true,
json: async () => ({ success: false })
});
window.location.search = `?flow=${flowId}&loading=true`;
render(<App />);
expect(
await screen.findByText(/something went wrong/i)
).toBeInTheDocument();
expect(
screen.queryByTestId('flow-loading-overlay')
).not.toBeInTheDocument();
});
});
});
Loading
Loading