This is a SDK for react application.
- @passflow/react
to install just type:
pnpm install
pnpm build
The visual source of truth lives in src/components/ui and is catalogued in Storybook.
pnpm storybookBefore reusing a new visual state in a flow, add it to the owning UI component and its co-located *.stories.tsx file. Verify the static catalog with:
pnpm build-storybookFlows and form controllers should map domain state and callbacks into library components rather than creating or styling controls in place. See CLAUDE.md for the repository boundary.
For local development and testing with a local version of the Passflow JS SDK, you need to:
- Clone the Passflow JS SDK repository in a sibling directory to this project.
- remove current dependecy
pnpm remove @passflow/core - Link folder with:
pnpm link ../passflow-js-sdk
pnpm installNow you can run watch mode in libraray mode and change it. It will compile every changes incrementally.
pnpm watchAfter all done, we need to unlink and return all to the original state
pnpm remove @passflow/core
pnpm unlink @passflow/core
pnpm install @passflow/coreFor local development and UI testing, you need to set up the Passflow environment:
- Set the
PASSFLOW_URLenvironment variable to point to your Passflow instance. - Set the
PASSFLOW_APP_IDenvironment variable - Run
pnpm devanmd all should works
Refer .env.example for more details.
we are using pnpm. Please ansure you have it in the system.
All UI behavior and Storybook state tests use Playwright. Install Chromium once:
pnpm exec playwright install chromiumRun the complete UI suite headlessly:
pnpm testOr launch Playwright's interactive test UI:
pnpm test:uiBoth scripts automatically start the demo app and Storybook. pnpm test uses the economical headless configuration, while pnpm test:ui retains traces and a final full-page screenshot for every successful test. Select a runnable leaf test in Playwright UI to inspect its action timeline, DOM snapshots, and screenshot attachment. Tests live in playwright/tests; component-state tests render the real Storybook stories rather than recreating components in a test-only harness. Vitest remains for non-visual hooks, controllers, and utility logic.
pnpm add @passflow/reactNo router is needed. PassflowFlow has its own small router, and works on its own or inside an app routed by React Router, TanStack Router, Wouter or anything else.
- React 18 or 19
- Node
^22.12.0 || ^24.0.0 || >=26.0.0
For a quick start with Passflow Cloud:
const passflowConfig: PassflowConfig = {
url: process.env.PASSFLOW_URL ?? 'http://localhost:5432',
appId: process.env.PASSFLOW_APP_ID ?? 'test_app_id',
createTenantForNewUser: true,
scopes: ['openid', 'email', 'profile', 'offline'],
};
export const PassflowProviderWrapper: FC<PropsWithChildren> = ({
children,
}) => {
const navigate = useNavigate(); // from react-router
return (
<PassflowProvider
url={passflowConfig.url}
appId={passflowConfig.appId}
createTenantForNewUser={passflowConfig.createTenantForNewUser}
scopes={passflowConfig.scopes}
navigate={(options) =>
navigate(
{
pathname: options.to,
search: options.search,
},
{ replace: options.replace }
)
}
router="react-router"
>
{children}
</PassflowProvider>
);
};
export const App = () => (
<BrowserRouter>
<PassflowProviderWrapper>
<PassflowFlow
federatedDisplayMode='redirect'
successAuthRedirect='https://jwt.io'
pathPrefix='/web'
/>
</PassflowProviderWrapper>
</BrowserRouter>
);<PassflowProvider> is also a full OIDC relying-party SDK out of the
box. When a parent_challenge_id query parameter is present on the
host page's URL (typical of a Passflow OP redirect to the login UI),
the provider:
-
Installs an interceptor on
window.fetchandXMLHttpRequest.prototype.{open,send}that injectsparent_challenge_idon every outbound Passflow auth POST body and follows server-returned{redirect_url}JSON responses withwindow.location.assign. Covers/auth/login,/auth/passwordless/*,/auth/passkey/*,/auth/2fa/*,/auth/federated/start, and/auth/session/exchange. -
Runs an exchange-on-mount probe. If the SDK has existing tokens AND the parent challenge allows it (
prompt=loginnot set,max_agenot exceeded), the provider POSTs/auth/session/exchangewith the current access token and navigates the browser when the server returns a redirect URL. This satisfies OIDC Core §3.1.2.1 / §3.1.2.4 (prompt=none,max_age,id_token_hint) without re-prompting an already-authenticated user. -
Renders a placeholder during exchange (theme via the
continuingToAppPlaceholderprop on PassflowFlow — TODO once the prop is wired through; today the default placeholder appears). -
Falls through to the login form when exchange is impossible or returns
403 upgrade_required. The login form gets an explanatory banner (also themeable).
Consumer code remains a single line of plumbing — mount
<PassflowProvider>, render <PassflowFlow> inside, done. No
manual interceptor wiring required.
See the project's docs/adr/0002-session-exchange-endpoint.md for
the server contract and docs/adr/0003-amr-acr-claims.md for the
authentication-context claims those tokens carry.
The whole sign-in experience on its own pages: sign-in, sign-up, one-time codes, magic links, password reset, invitations and two-factor. It needs no router and no setup:
<PassflowProvider url="https://auth.example.com" appId="your-app-id">
<PassflowFlow />
</PassflowProvider>| Prop | Type | Description |
|---|---|---|
| successAuthRedirect | string | URL to redirect after successful authentication. A full URL must be in the app's redirect URLs. |
| federatedDisplayMode | "modal" | "redirect" | Federated authentication display mode |
| pathPrefix | string | Where the flow lives, such as /auth. Optional: worked out from the first URL |
| routerType | "browser" | "hash" | "memory" | The history the flow keeps. Default browser |
| location, navigate | see below | Full control by the app's router. Optional, always together |
Paths. The flow's screens are <prefix>/signin, <prefix>/signup, <prefix>/verify-challenge-otp, <prefix>/verify-challenge-magic-link, <prefix>/forgot-password, <prefix>/forgot-password/success, <prefix>/password/reset, <prefix>/invitation, <prefix>/two-factor-verify, <prefix>/two-factor-setup and <prefix>/two-factor-setup-magic-link/:token. Without pathPrefix the flow takes the prefix from the first URL it sees: /auth/signin and /auth both give /auth, / gives none. A path under the prefix that names no screen goes to sign-in.
Inside an app's router. Mount the flow on a catch-all route; nothing else is needed. The flow moves the address bar and announces it with popstate, the event React Router, TanStack Router and Wouter listen to, so the app's router follows the flow and the flow follows the app, Back and Forward included:
// TanStack Router
const authRoute = createRoute({ getParentRoute: () => rootRoute, path: '/auth/$', component: () => <PassflowFlow /> });
// React Router
<Route path="/auth/*" element={<PassflowFlow />} />
// Wouter
<Route path="/auth" nest><PassflowFlow /></Route>routerType. browser (default) uses the address bar. hash keeps the flow's path after #. memory leaves the URL alone, for an embedded widget or tests.
Full control. When the app's router does not use the address bar (it runs in hash or memory mode), hand the flow its location and a way to move it. routerType is then not used:
// TanStack Router with a memory or hash history
function AuthPage() {
const router = useRouter();
const location = useRouterState({ select: (state) => state.location });
return (
<PassflowFlow
location={{ pathname: location.pathname, search: location.searchStr }}
navigate={(to, { replace }) => {
const href = `${to.pathname}${to.search}`;
if (replace) router.history.replace(href);
else router.history.push(href);
}}
/>
);
}TwoFactorSetupMagicLinkFlow can also be mounted on its own page. It reads the token from the URL (the segment after /two-factor-setup-magic-link/), or takes it as a token prop.
The sections below wire the app's router into the standalone form components (SignIn, SignUp, …) through PassflowProvider's navigate and router props. PassflowFlow does not need them.
Example of integration with React Router: PS: The example uses the Declarative approach.
import {
PassflowProvider,
SignIn,
SignUp,
ForgotPassword,
ForgotPasswordSuccess,
} from "@passflow/react";
import { BrowserRouter, Routes, Route, useNavigate } from "react-router";
const passflowConfig = {
url: import.meta.env.VITE_PASSFLOW_URL,
appId: import.meta.env.VITE_PASSFLOW_APP_ID,
createTenantForNewUser: true,
scopes: ["id", "offline", "email", "profile", "openid", "management"],
};
const PassflowProviderWrapper = ({ children }) => {
const navigate = useNavigate();
return (
<PassflowProvider
url={passflowConfig.url}
appId={passflowConfig.appId}
createTenantForNewUser={passflowConfig.createTenantForNewUser}
scopes={passflowConfig.scopes}
navigate={(options) =>
navigate(
{
pathname: options.to,
search: options.search,
},
{ replace: options.replace }
)
}
router="react-router"
>
{children}
</PassflowProvider>
);
};
export const App = () => (
<BrowserRouter>
<PassflowProviderWrapper>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/signin" element={<SignIn successAuthRedirect="/" signUpPath="/signup" />} />
<Route path="/signup" element={<SignUp successAuthRedirect="/" signInPath="/signin" />} />
<Route
path="/forgot-password"
element={
<ForgotPassword
successResetRedirect="/"
signInPath="/signin"
forgotPasswordSuccessPath="/forgot-password/success"
/>
}
/>
<Route path="/forgot-password/success" element={<ForgotPasswordSuccess />} />
{/* Add other routes here */}
</Routes>
</PassflowProviderWrapper>
</BrowserRouter>
);Example of integration with Wouter:
import {
PassflowProvider,
SignIn,
SignUp,
ForgotPassword,
ForgotPasswordSuccess,
} from "@passflow/react";
import { Switch, Route, useLocation } from "wouter";
const passflowConfig = {
url: import.meta.env.VITE_PASSFLOW_URL,
appId: import.meta.env.VITE_PASSFLOW_APP_ID,
createTenantForNewUser: true,
scopes: ["id", "offline", "email", "profile", "openid", "management"],
};
const PassflowProviderWrapper = ({ children }) => {
const [, navigate] = useLocation();
return (
<PassflowProvider
url={passflowConfig.url}
appId={passflowConfig.appId}
createTenantForNewUser={passflowConfig.createTenantForNewUser}
scopes={passflowConfig.scopes}
navigate={(options) => {
const searchParamWouter = options.search
? options.search.startsWith("?")
? options.search
: `?${options.search}`
: "";
navigate(`${options.to}${searchParamWouter}`, {
replace: options.replace,
});
}}
router="wouter"
>
{children}
</PassflowProvider>
);
};
export const App = () => (
<Switch>
<PassflowProviderWrapper>
<Route path="/" component={Home} />
<Route path="/signin" component={SignInWrapper} />
<Route path="/signup" component={SignUpWrapper} />
<Route path="/forgot-password" component={ForgotPasswordWrapper} />
<Route path="/forgot-password/success" component={ForgotPasswordSuccessWrapper} />
{/* Add other routes here */}
</PassflowProviderWrapper>
</Switch>
);Example of integration with TanStack Router: PS: The example uses Code-Based Routing.
// App.tsx
import { PassflowProviderWrapper, RouterProvider } from './providers';
export const App = () => (
<PassflowProviderWrapper>
<RouterProvider />
</PassflowProviderWrapper>
);
// PassflowProviderWrapper.tsx
import { PassflowProvider } from '@passflow/react';
const passflowConfig = {
url: import.meta.env.VITE_PASSFLOW_URL,
appId: import.meta.env.VITE_PASSFLOW_APP_ID,
createTenantForNewUser: true,
scopes: ['id', 'offline', 'email', 'profile', 'openid', 'management'],
};
export const PassflowProviderWrapper = ({ children }) => (
<PassflowProvider
url={passflowConfig.url}
appId={passflowConfig.appId}
createTenantForNewUser={passflowConfig.createTenantForNewUser}
scopes={passflowConfig.scopes}
router="tanstack-router"
>
{children}
</PassflowProvider>
);
// router/root.tsx
import { useNavigation } from '@passflow/react';
import { Outlet, useNavigate } from '@tanstack/react-router';
export const Root = () => {
const navigate = useNavigate();
const { setNavigate } = useNavigation();
useEffect(() => {
setNavigate((options) => navigate(options));
}, [navigate, setNavigate]);
return <Outlet />;
};
// router.tsx
import { QueryClient } from '@tanstack/react-query';
import { createRouter } from '@tanstack/react-router';
import { queryClient } from '../query';
import { routerTree } from './routes';
import { Passflow } from '@passflow/react';
export interface RouterContext {
queryClient: QueryClient;
passflow?: Passflow;
}
export const router = createRouter({
routeTree: routerTree,
context: {
queryClient,
passflow: undefined,
},
defaultPreload: 'intent',
});
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}
// routes.tsx
import { Outlet, createRootRouteWithContext, createRoute, redirect } from '@tanstack/react-router';
import { RouterContext } from './router';
import { Root } from './root';
import { About, Home } from '@/pages';
import { ForgotPassword, ForgotPasswordSuccess, SignIn, SignUp } from '@passflow/react';
import { RootLayout } from '@/layouts';
const redirectToSignin = () => {
throw redirect({
to: '/signin',
});
};
const rootRoute = createRootRouteWithContext<RouterContext>()({
component: Root,
notFoundComponent: () => <div>404 Not Found</div>,
});
// PUBLIC ROUTES
const publicRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'public',
component: () => <Outlet />,
});
const signInRoute = createRoute({
getParentRoute: () => publicRoute,
path: '/signin',
component: () => <SignIn successAuthRedirect='/' signUpPath='/signup' />,
});
const signUpRoute = createRoute({
getParentRoute: () => publicRoute,
path: '/signup',
component: () => <SignUp successAuthRedirect='/' signInPath='/signin' />,
});
const forgotPasswordRoute = createRoute({
getParentRoute: () => publicRoute,
path: '/forgot-password',
component: () => (
<ForgotPassword successResetRedirect='/' signInPath='/signin' forgotPasswordSuccessPath='/forgot-password/success' />
),
});
const forgotPasswordSuccessRoute = createRoute({
getParentRoute: () => publicRoute,
path: '/forgot-password/success',
component: () => <ForgotPasswordSuccess />,
});
{/* Add other PASSFLOW COMPONENTS routes here */}
// PROTECTED ROUTES
const protectedRoute = createRoute({
getParentRoute: () => rootRoute,
id: 'protected',
beforeLoad: async ({ context }) => {
const { passflow } = context;
await passflow?.session({
createSession: async (tokens) => {
console.log(tokens); // if session is created, this function will be called with the tokens
},
expiredSession: async () => {
console.log('expiredSession');
redirectToSignin(); // if session is expired and refresh token is not valid, redirect to signin
},
doRefresh: true,
});
},
component: () => <RootLayout />,
});
const dashboardRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/',
component: () => <Home />,
});
const aboutRoute = createRoute({
getParentRoute: () => protectedRoute,
path: '/about',
component: () => <About />,
});
{/* Add other protected routes here */}
export const routerTree = rootRoute.addChildren([
publicRoute.addChildren([signInRoute, signUpRoute, forgotPasswordRoute, forgotPasswordSuccessRoute]),
protectedRoute.addChildren([dashboardRoute, aboutRoute]),
]);| Prop | Type | Description |
|---|---|---|
| url | string | Passflow server URL |
| appId | string | Application ID |
| createTenantForNewUser | boolean | Whether to create a tenant for new users |
| scopes | string[] | Array of required scopes |
| router | "default" | "react-router" | "wouter" | "tanstack-router" | Router being used (optional) (default is native window navigation) |
| navigate | (options: NavigateOptions) => void | Navigation function (optional) (default is native window navigation) |
Component for user authentication.
| Prop | Type | Description | Default |
|---|---|---|---|
| successAuthRedirect | string | URL to redirect after successful sign in | Required |
| signUpPath | string | Path to sign up page (optional) | /signup |
| forgotPasswordPath | string | Path to forgot password page (optional) | /forgot-password |
| verifyMagicLinkPath | string | Path to verify magic link page (optional) | /verify-challenge-magic-link |
| verifyOTPPath | string | Path to verify OTP page (optional) | /verify-challenge-otp |
| federatedDisplayMode | "popup" | "redirect" | Display mode for federated authentication (optional) | "popup" |
Component for user registration.
| Prop | Type | Description | Default |
|---|---|---|---|
| successAuthRedirect | string | URL to redirect after successful sign up | Required |
| signInPath | string | Path to sign in page (optional) | /signin |
| verifyMagicLinkPath | string | Path to verify magic link page (optional) | /verify-challenge-magic-link |
| verifyOTPPath | string | Path to verify OTP page (optional) | /verify-challenge-otp |
| federatedDisplayMode | "popup" | "redirect" | Display mode for federated authentication (optional) | "popup" |
Component for password recovery initiation.
| Prop | Type | Description | Default |
|---|---|---|---|
| successResetRedirect | string | URL to redirect after successful password reset | Required |
| signInPath | string | Path to sign in page (optional) | /signin |
| forgotPasswordSuccessPath | string | Path to success page after initiating password reset (optional) | /forgot-password/success |
Component for password recovery success.
No props required.
Component for setting a new password.
| Prop | Type | Description | Default |
|---|---|---|---|
| successAuthRedirect | string | URL to redirect after successful password reset | Required |
Component for verifying magic link authentication.
No props required.
Component for OTP verification.
| Prop | Type | Description | Default |
|---|---|---|---|
| successAuthRedirect | string | URL to redirect after successful verification | Required |
| numInputs | number | Number of OTP input fields (optional) | 6 |
| shouldAutoFocus | boolean | Whether to autofocus the first input (optional) | true |
| signUpPath | string | Path to sign up page (optional) | /signup |
Component for accepting invitations and joining organizations.
| Prop | Type | Description | Default |
|---|---|---|---|
| successAuthRedirect | string | URL to redirect after successful join | Required |
| signInPath | string | Path to sign in page (optional) | /signin |
Hook for authentication management. Provides methods for checking authentication status, obtaining tokens, and logging out.
const { isAuthenticated, getTokens, logout, isLoading } = useAuth();| Parameter | Type | Description |
|---|---|---|
| initialRefresh | boolean (optional) |
Whether to refresh tokens on mount |
Returns:
| Property | Type | Description |
|---|---|---|
| isAuthenticated | () => boolean |
Current authentication status |
| getTokens | (doRefresh: boolean) => Promise<{ tokens: Tokens | undefined; parsedTokens: ParsedTokens | undefined; }> |
Function to get authentication tokens |
| logout | () => void |
Function to log out user |
| isLoading | boolean |
Loading state indicator |
Hook for accessing the Passflow SDK instance. Must be used within PassflowProvider.
const passflow = usePassflow();Returns:
| Type | Description |
|---|---|
Passflow |
Passflow SDK instance |
Hook for synchronizing state with Passflow SDK. Allows subscribing to token changes.
const tokens = usePassflowStore([PassflowEvent.SignIn, ...]);| Parameter | Type | Description |
|---|---|---|
| events | PassflowEvent[] (optional) |
Events to subscribe to |
Returns:
| Type | Description |
|---|---|
Tokens | undefined |
Current tokens state |
Hook for implementing sign-in functionality. Supports password, passkey, and passwordless authentication.
const { fetch, isLoading, isError, error } = useSignIn();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (payload: PassflowPasskeyAuthenticateStartPayload | PassflowSignInPayload | PassflowPasswordlessSignInPayload, type: 'passkey' | 'password' | 'passwordless') => Promise<boolean | string | PassflowPasswordlessResponse> |
Sign in function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for implementing registration functionality. Supports password, passkey, and passwordless registration.
const { fetch, isLoading, isError, error } = useSignUp();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (payload: PassflowPasskeyRegisterStartPayload | PassflowSignUpPayload | PassflowPasswordlessSignInPayload, type: 'passkey' | 'password' | 'passwordless') => Promise<boolean | PassflowPasswordlessResponse> |
Sign up function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for navigation between pages. Supports various routers (react-router, wouter, tanstack-router).
const { navigate, setNavigate } = useNavigation();Returns:
| Property | Type | Description |
|---|---|---|
| navigate | NavigateFunction |
Navigation function |
| setNavigate | (newNavigate: NavigateFunction | null) => void |
Function to update navigation handler |
Hook for working with federated authentication providers (OAuth).
const { federatedWithPopup, federatedWithRedirect } = useProvider(redirectUrl);| Parameter | Type | Description |
|---|---|---|
| redirectUrl | string |
URL to redirect after authentication |
Returns:
| Property | Type | Description |
|---|---|---|
| federatedWithPopup | (provider: Providers) => void |
Popup authentication function |
| federatedWithRedirect | (provider: Providers) => void |
Redirect authentication function |
Hook for resetting user password.
const { fetch, isLoading, isError, error } = useResetPassword();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (newPassword: string) => Promise<boolean> |
Password reset function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for managing user passkeys (create, edit, delete).
const { data, createUserPasskey, editUserPasskey, deleteUserPasskey } = useUserPasskeys();Returns:
| Property | Type | Description |
|---|---|---|
| data | PassflowUserPasskey[] |
List of user passkeys |
| createUserPasskey | (relyingPartyId: string) => Promise<void> |
Create passkey function |
| editUserPasskey | (newName: string, passkeyId: string) => Promise<void> |
Edit passkey function |
| deleteUserPasskey | (passkeyId: string) => Promise<void> |
Delete passkey function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| errorMessage | string |
Error message |
Hook for retrieving application settings and password policies.
const { appSettings, passwordPolicy } = useAppSettings();Returns:
| Property | Type | Description |
|---|---|---|
| appSettings | AppSettings | null |
Application settings |
| passwordPolicy | PassflowPasswordPolicySettings | null |
Password policy settings |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for redirecting to Passflow Cloud.
const redirect = useAuthCloudRedirect(cloudPassflowUrl);| Parameter | Type | Description |
|---|---|---|
| cloudPassflowUrl | string |
Passflow Cloud URL |
Returns:
| Type | Description |
|---|---|
() => void |
Redirect function |
Hook for initiating the password recovery process.
const { fetch, isLoading, isError, error } = useForgotPassword();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (payload: PassflowSendPasswordResetEmailPayload) => Promise<boolean> |
Password recovery function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for accepting organization invitations.
const { fetch, isLoading, isError, error } = useJoinInvite();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (token: string) => Promise<boolean> |
Join invitation function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for logging out of the system.
const { fetch, isLoading, isError, error } = useLogout();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | () => Promise<boolean> |
Logout function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for completing passwordless authentication.
const { fetch, isLoading, isError, error } = usePasswordlessComplete();Returns:
| Property | Type | Description |
|---|---|---|
| fetch | (payload: PassflowPasswordlessSignInCompletePayload) => Promise<PassflowValidationResponse | null> |
Complete passwordless auth function |
| isLoading | boolean |
Loading state |
| isError | boolean |
Error state |
| error | string |
Error message |
Hook for reacting to the end of a session, for example to redirect to your login page.
const { isSessionExpired, expiredReason, resetSessionExpired } = useSessionExpired({
onSessionExpired: (reason) => navigate('/signin'),
});SessionExpired fires only when the session has really ended: the last auth strike ('refresh_failed'), no successor token pair in time ('successor_timeout'), or another user's tokens in storage ('identity_changed'). A transient refresh failure (network error, 5xx) keeps the session and shows up as RefreshFailing / RefreshRecovered instead; use useRefreshStatus for that. The state resets on SignIn and Register.
| Parameter | Type | Description |
|---|---|---|
| onSessionExpired | (reason: SessionExpiredReason) => void (optional) |
Called when the session ends |
Returns:
| Property | Type | Description |
|---|---|---|
| isSessionExpired | boolean |
True once the session has ended |
| expiredReason | SessionExpiredReason | null |
Why it ended (SessionExpiredReason is exported from @passflow/core) |
| resetSessionExpired | () => void |
Clears the expired state |
Hook for showing a "connection trouble" notice while token refresh is failing transiently. The session is kept during the episode; show the notice on RefreshFailing and hide it on RefreshRecovered.
const { isFailing, since, nextRetryAt } = useRefreshStatus();Set by RefreshFailing. Cleared by RefreshRecovered, SessionExpired, SignOut, SignIn and Register. The episode is known only from events, so a component that mounts after RefreshFailing starts idle: call the hook in a component that lives as long as the app, near PassflowProvider.
Returns:
| Property | Type | Description |
|---|---|---|
| isFailing | boolean |
True inside a transient-failure episode |
| since | number | null |
Epoch ms of the episode's first failure |
| nextRetryAt | number | null |
Epoch ms of the pending retry, when known |