@djangocfg/layouts 2.1.547 → 2.1.549
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/package.json +12 -12
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +15 -2
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +16 -2
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +16 -2
- package/src/layouts/PrivateLayout/README.md +32 -0
- package/src/layouts/PrivateLayout/components/ForbiddenState.tsx +62 -0
- package/src/layouts/PrivateLayout/components/index.ts +2 -0
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +15 -1
- package/src/layouts/PrivateLayout/index.ts +2 -1
- package/src/layouts/PrivateLayout/types.ts +15 -0
- package/src/testing/MockAuthFormProvider.tsx +4 -0
package/README.md
CHANGED
|
@@ -66,6 +66,22 @@ provider surface and the Next.js vs Wails wiring, and the demo app
|
|
|
66
66
|
> **Admin** is not a separate layout — use `PrivateLayout` with an admin `sidebar`
|
|
67
67
|
> config in your `app/.../admin/layout.tsx`.
|
|
68
68
|
|
|
69
|
+
### Staff-only route groups
|
|
70
|
+
|
|
71
|
+
`PrivateLayout`'s guard checks the session. For a surface that also requires
|
|
72
|
+
staff, add `requireStaff` (resolved from `useAuth().isAdminUser`, the
|
|
73
|
+
`is_staff || is_superuser` fold):
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<PrivateLayout sidebar={sidebar} header={header} requireStaff>{children}</PrivateLayout>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A signed-in non-staff user gets `ForbiddenState` instead of the shell — **not a
|
|
80
|
+
redirect to the auth form**: they are already signed in, so signing in again
|
|
81
|
+
cannot grant the privilege, and since the guard saves the current URL first, a
|
|
82
|
+
redirect would loop back here. Pass `forbiddenFallback` to supply your own
|
|
83
|
+
screen. Details: [PrivateLayout README](./src/layouts/PrivateLayout/README.md).
|
|
84
|
+
|
|
69
85
|
### `AuthLayout` shells
|
|
70
86
|
|
|
71
87
|
Two visual variants, switchable via the `variant` prop:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.549",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -90,11 +90,11 @@
|
|
|
90
90
|
"check": "tsc --noEmit"
|
|
91
91
|
},
|
|
92
92
|
"peerDependencies": {
|
|
93
|
-
"@djangocfg/analytics": "^2.1.
|
|
94
|
-
"@djangocfg/api": "^2.1.
|
|
95
|
-
"@djangocfg/devtools": "^2.1.
|
|
96
|
-
"@djangocfg/i18n": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
93
|
+
"@djangocfg/analytics": "^2.1.549",
|
|
94
|
+
"@djangocfg/api": "^2.1.549",
|
|
95
|
+
"@djangocfg/devtools": "^2.1.549",
|
|
96
|
+
"@djangocfg/i18n": "^2.1.549",
|
|
97
|
+
"@djangocfg/ui-core": "^2.1.549",
|
|
98
98
|
"@hookform/resolvers": "^5.2.2",
|
|
99
99
|
"consola": "^3.4.2",
|
|
100
100
|
"lucide-react": "^0.545.0",
|
|
@@ -124,12 +124,12 @@
|
|
|
124
124
|
"uuid": "^11.1.1"
|
|
125
125
|
},
|
|
126
126
|
"devDependencies": {
|
|
127
|
-
"@djangocfg/analytics": "^2.1.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/devtools": "^2.1.
|
|
130
|
-
"@djangocfg/i18n": "^2.1.
|
|
131
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
132
|
-
"@djangocfg/ui-core": "^2.1.
|
|
127
|
+
"@djangocfg/analytics": "^2.1.549",
|
|
128
|
+
"@djangocfg/api": "^2.1.549",
|
|
129
|
+
"@djangocfg/devtools": "^2.1.549",
|
|
130
|
+
"@djangocfg/i18n": "^2.1.549",
|
|
131
|
+
"@djangocfg/typescript-config": "^2.1.549",
|
|
132
|
+
"@djangocfg/ui-core": "^2.1.549",
|
|
133
133
|
"@storybook/react-vite": "^10.5.0",
|
|
134
134
|
"@types/node": "^25.9.5",
|
|
135
135
|
"@types/react": "19.2.15",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { memo, useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useAppT } from '@djangocfg/i18n';
|
|
6
6
|
|
|
@@ -78,6 +78,15 @@ function OTPStepRaw() {
|
|
|
78
78
|
return `${local[0]}${'*'.repeat(Math.min(local.length - 1, 5))}@${domain}`;
|
|
79
79
|
}, [identifier]);
|
|
80
80
|
|
|
81
|
+
const autoSubmitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
82
|
+
|
|
83
|
+
useEffect(
|
|
84
|
+
() => () => {
|
|
85
|
+
if (autoSubmitTimerRef.current) clearTimeout(autoSubmitTimerRef.current);
|
|
86
|
+
},
|
|
87
|
+
[]
|
|
88
|
+
);
|
|
89
|
+
|
|
81
90
|
// Auto-submit on complete (state machine approach)
|
|
82
91
|
const handleOTPComplete = useCallback(
|
|
83
92
|
async (completedValue: string) => {
|
|
@@ -87,7 +96,11 @@ function OTPStepRaw() {
|
|
|
87
96
|
setSubmitState('submitting');
|
|
88
97
|
const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
|
|
89
98
|
|
|
90
|
-
|
|
99
|
+
// Tracked so leaving the step cancels it. This component unmounts when
|
|
100
|
+
// the user goes back to the identifier, and an uncancelled timer would
|
|
101
|
+
// submit the code they just abandoned.
|
|
102
|
+
autoSubmitTimerRef.current = setTimeout(async () => {
|
|
103
|
+
autoSubmitTimerRef.current = null;
|
|
91
104
|
try {
|
|
92
105
|
await handleOTPSubmit(fakeEvent);
|
|
93
106
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useCallback, useState } from 'react';
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
import { AUTH } from '../constants';
|
|
6
6
|
|
|
@@ -18,13 +18,27 @@ export const useCopyToClipboard = (
|
|
|
18
18
|
duration = AUTH.COPY_FEEDBACK_DURATION
|
|
19
19
|
): UseCopyToClipboardResult => {
|
|
20
20
|
const [copied, setCopied] = useState(false);
|
|
21
|
+
// One timer, cleared on unmount. Copying twice in quick succession stacked
|
|
22
|
+
// two, so the first cleared the flag while the second copy was still fresh.
|
|
23
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(
|
|
26
|
+
() => () => {
|
|
27
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
28
|
+
},
|
|
29
|
+
[]
|
|
30
|
+
);
|
|
21
31
|
|
|
22
32
|
const copy = useCallback(
|
|
23
33
|
async (text: string) => {
|
|
24
34
|
try {
|
|
25
35
|
await navigator.clipboard.writeText(text);
|
|
26
36
|
setCopied(true);
|
|
27
|
-
|
|
37
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
38
|
+
timerRef.current = setTimeout(() => {
|
|
39
|
+
timerRef.current = null;
|
|
40
|
+
setCopied(false);
|
|
41
|
+
}, duration);
|
|
28
42
|
} catch {
|
|
29
43
|
// Clipboard API not available
|
|
30
44
|
console.warn('Clipboard API not available');
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Authenticated shell: collapsible sidebar (icon rail vs expanded) + scrollable content.
|
|
5
5
|
* Toggle lives in the sidebar header on desktop; on narrow viewports a `SidebarTrigger` sits in `PrivateContent` (opens the mobile Sheet).
|
|
6
6
|
* Ctrl/Cmd + B still toggles the sidebar width.
|
|
7
|
+
*
|
|
8
|
+
* Guarded: no session redirects to `header.authPath`. With `requireStaff`, a
|
|
9
|
+
* signed-in non-staff user gets `ForbiddenState` instead — never a redirect,
|
|
10
|
+
* since signing in again cannot grant the privilege.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
'use client';
|
|
@@ -13,7 +17,7 @@ import React from 'react';
|
|
|
13
17
|
import { Preloader } from '@djangocfg/ui-core/components';
|
|
14
18
|
import { SidebarInset, SidebarProvider } from '@djangocfg/ui-core/components';
|
|
15
19
|
|
|
16
|
-
import { PrivateContent, PrivateSidebar } from './components';
|
|
20
|
+
import { ForbiddenState, PrivateContent, PrivateSidebar } from './components';
|
|
17
21
|
import { useAuthGuard } from './hooks';
|
|
18
22
|
import { useLayoutVisual } from './hooks';
|
|
19
23
|
import { useSidebarDefaultOpen } from './hooks';
|
|
@@ -49,10 +53,13 @@ export function PrivateLayout({
|
|
|
49
53
|
contentScroll = 'auto',
|
|
50
54
|
visual,
|
|
51
55
|
requireAuth = true,
|
|
56
|
+
requireStaff = false,
|
|
57
|
+
forbiddenFallback,
|
|
52
58
|
settings,
|
|
53
59
|
}: PrivateLayoutProps) {
|
|
54
|
-
const { isLoading, loadingText } = useAuthGuard({
|
|
60
|
+
const { isLoading, isForbidden, loadingText } = useAuthGuard({
|
|
55
61
|
requireAuth,
|
|
62
|
+
requireStaff,
|
|
56
63
|
authPath: header?.authPath,
|
|
57
64
|
});
|
|
58
65
|
|
|
@@ -73,6 +80,13 @@ export function PrivateLayout({
|
|
|
73
80
|
);
|
|
74
81
|
}
|
|
75
82
|
|
|
83
|
+
// Before the shell: a forbidden user must never reach the sidebar or the
|
|
84
|
+
// children, or every request they trigger returns 403 and the screen cannot
|
|
85
|
+
// be told apart from a backend outage.
|
|
86
|
+
if (isForbidden) {
|
|
87
|
+
return <>{forbiddenFallback ?? <ForbiddenState />}</>;
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
return (
|
|
77
91
|
<SidebarProvider
|
|
78
92
|
defaultOpen={defaultOpen}
|
|
@@ -17,6 +17,38 @@ The auth guard redirects to `header.authPath` when there's no session. Pass `req
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
+
## Staff-only surfaces
|
|
21
|
+
|
|
22
|
+
Add `requireStaff` when the whole route group is staff-only:
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
<PrivateLayout sidebar={sidebar} header={header} requireStaff>{children}</PrivateLayout>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It resolves from `useAuth().isAdminUser` — the `is_staff || is_superuser` fold,
|
|
29
|
+
never one flag on its own.
|
|
30
|
+
|
|
31
|
+
| Prop | Type | Default | Role |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `requireStaff` | `boolean` | `false` | Require staff/admin on top of a session. |
|
|
34
|
+
| `forbiddenFallback` | `ReactNode` | `<ForbiddenState />` | Replaces the built-in screen. |
|
|
35
|
+
|
|
36
|
+
**On failure the forbidden state renders instead of the shell — there is no
|
|
37
|
+
redirect to the auth form.** The user is already signed in, so the sign-in form
|
|
38
|
+
cannot fix anything; and because the guard saves the current URL before
|
|
39
|
+
redirecting, a re-login on the same account would land straight back here, in a
|
|
40
|
+
loop. The default `ForbiddenState` names the signed-in account and offers sign
|
|
41
|
+
out, which is the one action that helps: it lets them switch accounts.
|
|
42
|
+
|
|
43
|
+
Gate the shell rather than each control inside it, so the wall renders **before
|
|
44
|
+
any request fires**. Without it a non-staff operator reaches the surface, every
|
|
45
|
+
call returns `403`, and the screen is indistinguishable from a backend outage.
|
|
46
|
+
|
|
47
|
+
Presentation, not protection: the endpoints behind the surface must withhold the
|
|
48
|
+
data themselves. The gate decides what is *offered*, never what is *permitted*.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
20
52
|
## Wiring (canonical)
|
|
21
53
|
|
|
22
54
|
Mount it in the **route-group `layout.tsx`** that owns the authenticated section
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forbidden State
|
|
3
|
+
*
|
|
4
|
+
* Shown when a signed-in user lacks the privilege a route requires
|
|
5
|
+
* (`requireStaff`). Deliberately NOT a redirect to the sign-in form: the
|
|
6
|
+
* session is valid, so signing in again changes nothing — and since the guard
|
|
7
|
+
* saves the current URL first, a redirect would loop straight back here.
|
|
8
|
+
*
|
|
9
|
+
* It renders before any request fires, so the operator sees "wrong account"
|
|
10
|
+
* instead of a screen full of 403s indistinguishable from a backend outage.
|
|
11
|
+
* Sign out is the only action that helps: it lets them switch accounts.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
|
|
18
|
+
import { ShieldAlert } from 'lucide-react';
|
|
19
|
+
|
|
20
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
21
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
22
|
+
|
|
23
|
+
export interface ForbiddenStateProps {
|
|
24
|
+
/** Heading. Defaults to a staff-access message. */
|
|
25
|
+
title?: string;
|
|
26
|
+
/** Explanation under the heading. */
|
|
27
|
+
description?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function ForbiddenState({
|
|
31
|
+
title = 'Staff access required',
|
|
32
|
+
description = 'Your account is signed in but does not have staff permissions for this area. Sign out and use a staff account, or ask an administrator to grant access.',
|
|
33
|
+
}: ForbiddenStateProps) {
|
|
34
|
+
const { user, logout } = useAuth();
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
role="alert"
|
|
39
|
+
className="flex min-h-svh flex-col items-center justify-center gap-4 p-6 text-center"
|
|
40
|
+
>
|
|
41
|
+
<div className="flex size-12 items-center justify-center rounded-full bg-destructive/10">
|
|
42
|
+
<ShieldAlert className="size-6 text-destructive" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<h1 className="text-lg font-semibold">{title}</h1>
|
|
47
|
+
<p className="max-w-md text-sm text-muted-foreground">{description}</p>
|
|
48
|
+
{/* Naming the account is the fastest way to spot the usual cause:
|
|
49
|
+
signed in on the right site with the wrong account. */}
|
|
50
|
+
{user?.email && (
|
|
51
|
+
<p className="text-xs text-muted-foreground">
|
|
52
|
+
Signed in as <span className="font-medium">{user.email}</span>
|
|
53
|
+
</p>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<Button variant="outline" size="sm" onClick={logout}>
|
|
58
|
+
Sign out
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
export { PrivateSidebar } from './PrivateSidebar';
|
|
6
6
|
export { PrivateContent } from './PrivateContent';
|
|
7
|
+
export { ForbiddenState } from './ForbiddenState';
|
|
8
|
+
export type { ForbiddenStateProps } from './ForbiddenState';
|
|
7
9
|
export { SidebarBrand } from './SidebarBrand';
|
|
8
10
|
export { SidebarBrandSwitcher } from './SidebarBrandSwitcher';
|
|
9
11
|
export { SidebarNavGroup } from './SidebarNavGroup';
|
|
@@ -14,10 +14,16 @@ import {
|
|
|
14
14
|
resolveGuardIsLoading,
|
|
15
15
|
resolveGuardIsAuthenticated,
|
|
16
16
|
shouldRedirectToAuth,
|
|
17
|
+
isForbidden,
|
|
17
18
|
} from '@djangocfg/api/auth';
|
|
18
19
|
|
|
19
20
|
interface UseAuthGuardOptions {
|
|
20
21
|
requireAuth?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Require staff/admin (`is_staff || is_superuser`) on top of a session.
|
|
24
|
+
* A signed-in non-staff user gets the forbidden state, not a redirect.
|
|
25
|
+
*/
|
|
26
|
+
requireStaff?: boolean;
|
|
21
27
|
/** Explicit override; defaults to the app's resolved `useAuth().routes.auth`. */
|
|
22
28
|
authPath?: string;
|
|
23
29
|
/**
|
|
@@ -34,16 +40,19 @@ interface UseAuthGuardResult {
|
|
|
34
40
|
isLoading: boolean;
|
|
35
41
|
/** Whether the user is authenticated (or auth is not required) */
|
|
36
42
|
isAuthenticated: boolean;
|
|
43
|
+
/** Signed in, but lacking the privilege this route requires. */
|
|
44
|
+
isForbidden: boolean;
|
|
37
45
|
/** Loading text for the preloader */
|
|
38
46
|
loadingText: string;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export function useAuthGuard({
|
|
42
50
|
requireAuth = true,
|
|
51
|
+
requireStaff = false,
|
|
43
52
|
authPath,
|
|
44
53
|
loadingTimeoutMs = 8000,
|
|
45
54
|
}: UseAuthGuardOptions): UseAuthGuardResult {
|
|
46
|
-
const { isAuthenticated, isLoading: authLoading, saveRedirectUrl, routes } = useAuth();
|
|
55
|
+
const { isAuthenticated, isLoading: authLoading, isAdminUser, saveRedirectUrl, routes } = useAuth();
|
|
47
56
|
const resolvedAuthPath = authPath ?? routes.auth;
|
|
48
57
|
const router = useRouter();
|
|
49
58
|
const [isRedirecting, setIsRedirecting] = useState(false);
|
|
@@ -78,6 +87,8 @@ export function useAuthGuard({
|
|
|
78
87
|
authLoading: effectiveAuthLoading,
|
|
79
88
|
isRedirecting,
|
|
80
89
|
isAuthenticated,
|
|
90
|
+
requireStaff,
|
|
91
|
+
isAdminUser,
|
|
81
92
|
};
|
|
82
93
|
|
|
83
94
|
useEffect(() => {
|
|
@@ -96,6 +107,9 @@ export function useAuthGuard({
|
|
|
96
107
|
return {
|
|
97
108
|
isLoading,
|
|
98
109
|
isAuthenticated: resolveGuardIsAuthenticated(guardState),
|
|
110
|
+
// Never redirects: shouldRedirectToAuth requires !isAuthenticated, so a
|
|
111
|
+
// forbidden user is structurally excluded from the redirect effect above.
|
|
112
|
+
isForbidden: isForbidden(guardState),
|
|
99
113
|
loadingText,
|
|
100
114
|
};
|
|
101
115
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
export { PrivateLayout } from './PrivateLayout';
|
|
6
6
|
export { PrivateLayoutProvider, usePrivateLayoutContext } from './context';
|
|
7
|
-
export { SidebarBrandSwitcher } from './components';
|
|
7
|
+
export { SidebarBrandSwitcher, ForbiddenState } from './components';
|
|
8
|
+
export type { ForbiddenStateProps } from './components';
|
|
8
9
|
export type {
|
|
9
10
|
PrivateLayoutProps,
|
|
10
11
|
SidebarItem,
|
|
@@ -223,6 +223,21 @@ export interface PrivateLayoutProps {
|
|
|
223
223
|
* embeds where there's no real session. Default `true` (guard on).
|
|
224
224
|
*/
|
|
225
225
|
requireAuth?: boolean;
|
|
226
|
+
/**
|
|
227
|
+
* Require staff/admin (`is_staff || is_superuser`) on top of a session.
|
|
228
|
+
* A signed-in non-staff user gets the forbidden state instead of the shell —
|
|
229
|
+
* not a redirect, which would loop since signing in again cannot grant the
|
|
230
|
+
* privilege. Default `false`.
|
|
231
|
+
*
|
|
232
|
+
* Presentation only: the endpoints behind this surface must withhold the
|
|
233
|
+
* data themselves. The gate decides what is offered, never what is permitted.
|
|
234
|
+
*/
|
|
235
|
+
requireStaff?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Replaces the built-in "not authorised" screen shown when `requireStaff`
|
|
238
|
+
* fails. Omit for the default `ForbiddenState`.
|
|
239
|
+
*/
|
|
240
|
+
forbiddenFallback?: ReactNode;
|
|
226
241
|
/**
|
|
227
242
|
* Mount the global SettingsDialog (Claude-style master/detail settings modal,
|
|
228
243
|
* hash-URL driven, openable via `useSettingsDialog()`). Pass a config object
|
|
@@ -77,6 +77,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
77
77
|
}) => {
|
|
78
78
|
const [step, setStep] = useState<AuthStep>(initialStep);
|
|
79
79
|
const [identifier, setIdentifier] = useState(initialIdentifier);
|
|
80
|
+
const [password, setPassword] = useState('');
|
|
80
81
|
const [otp, setOtp] = useState(initialOtp);
|
|
81
82
|
const [isLoading, setIsLoading] = useState(initialLoading);
|
|
82
83
|
const [acceptedTerms, setAcceptedTerms] = useState(initialAcceptedTerms);
|
|
@@ -100,6 +101,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
100
101
|
return {
|
|
101
102
|
// State
|
|
102
103
|
identifier,
|
|
104
|
+
password,
|
|
103
105
|
otp,
|
|
104
106
|
isLoading,
|
|
105
107
|
acceptedTerms,
|
|
@@ -118,6 +120,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
118
120
|
|
|
119
121
|
// State handlers
|
|
120
122
|
setIdentifier,
|
|
123
|
+
setPassword,
|
|
121
124
|
setOtp,
|
|
122
125
|
setAcceptedTerms,
|
|
123
126
|
setRememberMe,
|
|
@@ -137,6 +140,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
|
|
|
137
140
|
// step never changes from under the storyteller. To preview a
|
|
138
141
|
// different step, render a new story with `step="..."`.
|
|
139
142
|
handleIdentifierSubmit: async (e) => stop(e),
|
|
143
|
+
handlePasswordSubmit: async (e) => stop(e),
|
|
140
144
|
handleOTPSubmit: async (e) => stop(e),
|
|
141
145
|
handleResendOTP: async () => {},
|
|
142
146
|
handleBackToIdentifier: () => setStep('identifier'),
|