@djangocfg/layouts 2.1.264 → 2.1.267
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 +113 -4
- package/package.json +19 -18
- package/src/hooks/index.ts +1 -1
- package/src/hooks/usePathnameWithoutLocale.ts +35 -19
- package/src/layouts/AppLayout/AppLayout.tsx +15 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +50 -6
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +206 -235
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -3
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +22 -106
- package/src/layouts/ProfileLayout/components/EditableField.tsx +15 -10
- package/src/layouts/ProfileLayout/components/Section.tsx +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +255 -215
- package/src/layouts/ProfileLayout/context.tsx +108 -16
- package/src/layouts/ProfileLayout/index.ts +1 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +18 -0
- package/src/layouts/PublicLayout/components/NavActions.tsx +50 -0
- package/src/layouts/PublicLayout/components/NavBrand.tsx +26 -0
- package/src/layouts/PublicLayout/components/NavDesktopItems.tsx +207 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +44 -6
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +199 -396
- package/src/layouts/PublicLayout/hooks/index.ts +5 -1
- package/src/layouts/PublicLayout/hooks/useDropdownMenu.ts +58 -0
- package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +61 -0
- package/src/layouts/PublicLayout/hooks/useNavbarViewportVars.ts +46 -0
- package/src/layouts/PublicLayout/index.ts +4 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +17 -0
- package/src/utils/pathMatcher.ts +6 -3
- package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +0 -2
- package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +0 -3
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +0 -19
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +0 -5
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ import { AppLayout } from '@djangocfg/layouts';
|
|
|
104
104
|
public: { component: PublicLayout, enabledPath: ['/', '/legal', '/contact'] },
|
|
105
105
|
private: { component: PrivateLayout, enabledPath: ['/dashboard'] },
|
|
106
106
|
admin: { component: AdminLayout, enabledPath: '/admin' },
|
|
107
|
-
noLayoutPaths: ['/embed'],
|
|
107
|
+
noLayoutPaths: ['/embed', '/ui'], // fullscreen, no navbar/footer
|
|
108
108
|
}}
|
|
109
109
|
baseApp={{ project: 'my-app', theme: { defaultTheme: 'system' }, auth: { apiUrl: '…' } }}
|
|
110
110
|
i18n={{ locale, locales, onLocaleChange }}
|
|
@@ -113,21 +113,130 @@ import { AppLayout } from '@djangocfg/layouts';
|
|
|
113
113
|
</AppLayout>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
+
> **`i18n` is required for correct path matching.**
|
|
117
|
+
> `AppLayout` strips the locale prefix from the pathname before matching `enabledPath` / `noLayoutPaths`.
|
|
118
|
+
> It uses `i18n.locale` for exact stripping — without it the fallback regex can misfire on
|
|
119
|
+
> 2-letter path segments (e.g. `/ui/*` being treated as locale `ui`).
|
|
120
|
+
> Always pass `i18n` when using next-intl or any locale-prefixed routing.
|
|
121
|
+
|
|
116
122
|
---
|
|
117
123
|
|
|
118
124
|
## Layouts (import from `@djangocfg/layouts`)
|
|
119
125
|
|
|
120
126
|
| Component | Use |
|
|
121
127
|
|-----------|-----|
|
|
122
|
-
| **PublicLayout** | Marketing / docs — slots: **`navbar`**, **`footer`**, **`contentTopSpacing`**, **`contentBottomSpacing`** |
|
|
123
|
-
| **PublicNavbar** / **PublicFooter** |
|
|
128
|
+
| **PublicLayout** | Marketing / docs — slots: **`navbar`**, **`footer`**, **`backgroundSlot`**, **`contentTopSpacing`**, **`contentBottomSpacing`** |
|
|
129
|
+
| **PublicNavbar** / **PublicFooter** | See `PublicNavbarConfig` below |
|
|
124
130
|
| **PrivateLayout** | App shell — sidebar + header |
|
|
125
131
|
| **AuthLayout** | Sign-in flows |
|
|
126
132
|
| **AdminLayout** | Admin console |
|
|
127
|
-
| **ProfileLayout** | Profile
|
|
133
|
+
| **ProfileLayout** | Profile page with avatar, editable fields, 2FA, and slot/tab system |
|
|
134
|
+
|
|
135
|
+
**`PublicLayout` — `backgroundSlot`**
|
|
136
|
+
|
|
137
|
+
Pass any `ReactNode` as `backgroundSlot` to render a full-viewport layer *behind* the sticky navbar and all page content — without affecting layout flow.
|
|
138
|
+
The recommended pattern is `fixed inset-0 -z-10 pointer-events-none`:
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
<PublicLayout
|
|
142
|
+
contentTopSpacing="none"
|
|
143
|
+
contentBottomSpacing="none"
|
|
144
|
+
backgroundSlot={
|
|
145
|
+
<div
|
|
146
|
+
className="pointer-events-none fixed inset-0 -z-10"
|
|
147
|
+
style={{
|
|
148
|
+
background:
|
|
149
|
+
'radial-gradient(ellipse 55% 50% at 10% 0%, rgba(139,92,246,0.13) 0%, transparent 55%),' +
|
|
150
|
+
'radial-gradient(ellipse 40% 30% at 85% 75%, rgba(6,182,212,0.06) 0%, transparent 55%)',
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
}
|
|
154
|
+
navbar={<PublicNavbar config={…} />}
|
|
155
|
+
>
|
|
156
|
+
<HeroSection />
|
|
157
|
+
</PublicLayout>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Because the element is `fixed`, it covers the full viewport including the sticky navbar area, and `backdrop-blur` on the navbar lets the gradient show through.
|
|
161
|
+
|
|
162
|
+
---
|
|
128
163
|
|
|
129
164
|
**Brand:** `ThemeBrandMark` / **`ThemeBrandMarkImg`** for logo slots.
|
|
130
165
|
|
|
166
|
+
### PublicNavbar — layout variants & scroll behaviour
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
<PublicNavbar config={{
|
|
170
|
+
brand: <Logo />,
|
|
171
|
+
navigation: navItems,
|
|
172
|
+
userMenu,
|
|
173
|
+
|
|
174
|
+
// Visual
|
|
175
|
+
navbarVariant: 'floating', // 'floating' | 'flush'
|
|
176
|
+
navbarPosition: 'sticky', // 'sticky' | 'fixed' | 'static'
|
|
177
|
+
navbarHeight: 'md', // 'sm' | 'md' | 'lg'
|
|
178
|
+
|
|
179
|
+
// Desktop nav arrangement
|
|
180
|
+
navLayout: 'default',
|
|
181
|
+
// 'default' → brand left | nav centered (absolute) | actions right
|
|
182
|
+
// 'brand-left' → brand left | nav after brand | actions pushed right
|
|
183
|
+
// 'centered' → brand + nav + actions all centered in one row
|
|
184
|
+
// 'split' → brand left | actions right | no desktop nav (drawer only)
|
|
185
|
+
|
|
186
|
+
// Scroll behaviour
|
|
187
|
+
hideNavOnScroll: true, // slide up on scroll-down, back on scroll-up
|
|
188
|
+
transparent: true, // transparent at page top, opaque after threshold
|
|
189
|
+
transparentThreshold: 40, // px, default 40
|
|
190
|
+
}} />
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Exported hooks** (for custom navbars):
|
|
194
|
+
|
|
195
|
+
| Hook | Role |
|
|
196
|
+
|------|------|
|
|
197
|
+
| `useNavbarScroll(opts)` | Returns `{ hidden, scrolled }` driven by scroll position |
|
|
198
|
+
| `useDropdownMenu()` | Hover open/close timers for desktop dropdowns |
|
|
199
|
+
| `useNavbarViewportVars(ref, deps)` | Sets `--public-navbar-mobile-drawer-top/max-height` CSS vars |
|
|
200
|
+
|
|
201
|
+
### ProfileLayout — slots & tabs
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { ProfileLayout } from '@djangocfg/layouts';
|
|
205
|
+
import type { ProfileTab, ProfileSlots } from '@djangocfg/layouts';
|
|
206
|
+
|
|
207
|
+
const tabs: ProfileTab[] = [
|
|
208
|
+
{
|
|
209
|
+
value: 'billing',
|
|
210
|
+
label: 'Billing',
|
|
211
|
+
content: <BillingSection />,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
const slots: ProfileSlots = {
|
|
216
|
+
headerBadge: <Badge>Pro</Badge>, // next to user name
|
|
217
|
+
headerMenuItems: <DropdownMenuItem>…</DropdownMenuItem>, // in ⋯ menu
|
|
218
|
+
headerAfter: <OnboardingBanner />, // below avatar, above tabs
|
|
219
|
+
footer: <LinkedAccounts />, // below all tab content
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
<ProfileLayout
|
|
223
|
+
enable2FA
|
|
224
|
+
enableDeleteAccount
|
|
225
|
+
tabs={tabs}
|
|
226
|
+
slots={slots}
|
|
227
|
+
/>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Prop | Type | Description |
|
|
231
|
+
|------|------|-------------|
|
|
232
|
+
| `enable2FA` | `boolean` | Show Security tab with 2FA management |
|
|
233
|
+
| `enableDeleteAccount` | `boolean` | Show Delete account in `⋯` menu |
|
|
234
|
+
| `tabs` | `ProfileTab[]` | Extra tabs appended after built-in ones |
|
|
235
|
+
| `slots.headerBadge` | `ReactNode` | Rendered next to the user name (plan, role…) |
|
|
236
|
+
| `slots.headerMenuItems` | `ReactNode` | Extra `DropdownMenuItem`s in the `⋯` menu |
|
|
237
|
+
| `slots.headerAfter` | `ReactNode` | Below avatar row, above tabs |
|
|
238
|
+
| `slots.footer` | `ReactNode` | Below all tab content |
|
|
239
|
+
|
|
131
240
|
---
|
|
132
241
|
|
|
133
242
|
## i18n on AppLayout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.267",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,14 +74,14 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/
|
|
80
|
-
"@djangocfg/
|
|
81
|
-
"@djangocfg/
|
|
82
|
-
"@djangocfg/ui-core": "^2.1.
|
|
83
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
84
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.267",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.267",
|
|
79
|
+
"@djangocfg/debuger": "^2.1.267",
|
|
80
|
+
"@djangocfg/i18n": "^2.1.267",
|
|
81
|
+
"@djangocfg/monitor": "^2.1.267",
|
|
82
|
+
"@djangocfg/ui-core": "^2.1.267",
|
|
83
|
+
"@djangocfg/ui-nextjs": "^2.1.267",
|
|
84
|
+
"@djangocfg/ui-tools": "^2.1.267",
|
|
85
85
|
"@hookform/resolvers": "^5.2.2",
|
|
86
86
|
"consola": "^3.4.2",
|
|
87
87
|
"lucide-react": "^0.545.0",
|
|
@@ -103,21 +103,22 @@
|
|
|
103
103
|
}
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
|
+
"libphonenumber-js": "^1.12.24",
|
|
106
107
|
"nextjs-toploader": "^3.9.17",
|
|
107
108
|
"qrcode.react": "^4.2.0",
|
|
108
109
|
"react-ga4": "^2.1.0",
|
|
109
110
|
"uuid": "^11.1.0"
|
|
110
111
|
},
|
|
111
112
|
"devDependencies": {
|
|
112
|
-
"@djangocfg/api": "^2.1.
|
|
113
|
-
"@djangocfg/
|
|
114
|
-
"@djangocfg/
|
|
115
|
-
"@djangocfg/
|
|
116
|
-
"@djangocfg/
|
|
117
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
118
|
-
"@djangocfg/ui-core": "^2.1.
|
|
119
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
120
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
113
|
+
"@djangocfg/api": "^2.1.267",
|
|
114
|
+
"@djangocfg/centrifugo": "^2.1.267",
|
|
115
|
+
"@djangocfg/debuger": "^2.1.267",
|
|
116
|
+
"@djangocfg/i18n": "^2.1.267",
|
|
117
|
+
"@djangocfg/monitor": "^2.1.267",
|
|
118
|
+
"@djangocfg/typescript-config": "^2.1.267",
|
|
119
|
+
"@djangocfg/ui-core": "^2.1.267",
|
|
120
|
+
"@djangocfg/ui-nextjs": "^2.1.267",
|
|
121
|
+
"@djangocfg/ui-tools": "^2.1.267",
|
|
121
122
|
"@types/node": "^24.7.2",
|
|
122
123
|
"@types/react": "^19.1.0",
|
|
123
124
|
"@types/react-dom": "^19.1.0",
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { usePathnameWithoutLocale
|
|
1
|
+
export { usePathnameWithoutLocale } from './usePathnameWithoutLocale';
|
|
2
2
|
export { useLogout } from './useLogout';
|
|
@@ -4,36 +4,52 @@ import { usePathname } from 'next/navigation';
|
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Normalize pathname: remove trailing slash except for root "/".
|
|
8
|
+
* Works regardless of Next.js trailingSlash config.
|
|
9
9
|
*/
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (match) {
|
|
15
|
-
const rest = pathname.slice(match[0].length - 1) || '/';
|
|
16
|
-
return rest.startsWith('/') ? rest : '/' + rest;
|
|
17
|
-
}
|
|
10
|
+
function normalize(p: string): string {
|
|
11
|
+
return p.length > 1 ? p.replace(/\/+$/, '') : p;
|
|
12
|
+
}
|
|
18
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Strip locale prefix from pathname using known locale string.
|
|
16
|
+
*/
|
|
17
|
+
function stripLocale(pathname: string, locale: string): string {
|
|
18
|
+
const prefix = '/' + locale;
|
|
19
|
+
if (pathname === prefix || pathname === prefix + '/') return '/';
|
|
20
|
+
if (pathname.startsWith(prefix + '/')) return pathname.slice(prefix.length);
|
|
21
|
+
// Path already has no locale prefix (next-intl localePrefix:'as-needed' for default locale)
|
|
19
22
|
return pathname;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* usePathnameWithoutLocale
|
|
24
27
|
*
|
|
25
|
-
* Returns pathname without locale prefix.
|
|
26
|
-
*
|
|
28
|
+
* Returns the current pathname without locale prefix, normalized (no trailing slash).
|
|
29
|
+
*
|
|
30
|
+
* Pass `locale` (from i18n config / useLocaleSwitcher) for exact stripping.
|
|
31
|
+
* Without locale, falls back to regex — known to misfire on 2-letter path segments like /ui.
|
|
27
32
|
*
|
|
28
33
|
* @example
|
|
29
|
-
* // URL: /
|
|
30
|
-
*
|
|
31
|
-
* // pathname = '/private/dashboard'
|
|
34
|
+
* // URL: /en/ui/playground, locale="en" → "/ui/playground"
|
|
35
|
+
* // URL: /ui/playground/ (no prefix) → "/ui/playground"
|
|
32
36
|
*/
|
|
33
|
-
export function usePathnameWithoutLocale(): string {
|
|
37
|
+
export function usePathnameWithoutLocale(locale?: string): string {
|
|
34
38
|
const pathname = usePathname();
|
|
35
39
|
|
|
36
|
-
return useMemo(() =>
|
|
37
|
-
|
|
40
|
+
return useMemo(() => {
|
|
41
|
+
if (locale) {
|
|
42
|
+
return normalize(stripLocale(pathname, locale));
|
|
43
|
+
}
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
// Fallback regex — only when locale is unknown
|
|
46
|
+
const match = pathname.match(/^\/[a-z]{2}(-[A-Z]{2})?(\/|$)/);
|
|
47
|
+
if (match) {
|
|
48
|
+
const rest = pathname.slice(match[0].length - 1) || '/';
|
|
49
|
+
const withSlash = rest.startsWith('/') ? rest : '/' + rest;
|
|
50
|
+
return normalize(withSlash);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return normalize(pathname);
|
|
54
|
+
}, [pathname, locale]);
|
|
55
|
+
}
|
|
@@ -75,6 +75,14 @@ export interface AppLayoutPublicChrome {
|
|
|
75
75
|
topSpacing?: PublicMainTopSpacing;
|
|
76
76
|
bottomSpacing?: PublicMainBottomSpacing;
|
|
77
77
|
};
|
|
78
|
+
/**
|
|
79
|
+
* Full-viewport background layer rendered behind the navbar and all page content.
|
|
80
|
+
* Pass a `fixed inset-0 -z-10 pointer-events-none` element — it covers the whole
|
|
81
|
+
* viewport (including the sticky navbar area) without affecting layout flow.
|
|
82
|
+
*
|
|
83
|
+
* Set per-page via `AppLayout publicChrome` or directly on `PublicSiteLayout`.
|
|
84
|
+
*/
|
|
85
|
+
backgroundSlot?: ReactNode;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
function mergePartialNavbar(
|
|
@@ -120,8 +128,10 @@ export function mergeAppLayoutPublicChrome(
|
|
|
120
128
|
root?.main || fromLayouts?.main
|
|
121
129
|
? { ...root?.main, ...fromLayouts?.main }
|
|
122
130
|
: undefined;
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
// backgroundSlot: layouts-level overrides root (more specific wins)
|
|
132
|
+
const backgroundSlot = fromLayouts?.backgroundSlot ?? root?.backgroundSlot;
|
|
133
|
+
if (!navbar && !footer && !main && !backgroundSlot) return undefined;
|
|
134
|
+
return { navbar, footer, ...(main ? { main } : {}), ...(backgroundSlot ? { backgroundSlot } : {}) };
|
|
125
135
|
}
|
|
126
136
|
|
|
127
137
|
/**
|
|
@@ -291,8 +301,9 @@ function AppLayoutContent({
|
|
|
291
301
|
i18n,
|
|
292
302
|
publicChrome,
|
|
293
303
|
}: AppLayoutContentProps) {
|
|
294
|
-
// Use pathname without locale prefix for route matching
|
|
295
|
-
|
|
304
|
+
// Use pathname without locale prefix for route matching.
|
|
305
|
+
// Pass locale from i18n so strip is exact — avoids regex misfiring on /ui, /ko, etc.
|
|
306
|
+
const pathname = usePathnameWithoutLocale(i18n?.locale);
|
|
296
307
|
|
|
297
308
|
// Merge authPath into noLayoutPaths — auth pages are always fullscreen
|
|
298
309
|
const effectiveNoLayoutPaths = useMemo(() => {
|
|
@@ -15,12 +15,8 @@ export interface SetupStepProps {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* SetupStep - Orchestrator for 2FA setup flow
|
|
19
|
-
*
|
|
20
|
-
* Delegates rendering to focused sub-components:
|
|
21
|
-
* - SetupLoading: Initial loading state
|
|
22
|
-
* - SetupQRCode: QR code scanning
|
|
23
|
-
* - SetupComplete: Backup codes display
|
|
18
|
+
* SetupStep - Orchestrator for 2FA setup flow (requires AuthFormContext).
|
|
19
|
+
* For use outside AuthLayout use SetupStepStandalone.
|
|
24
20
|
*/
|
|
25
21
|
export const SetupStep: React.FC<SetupStepProps> = ({ onComplete, onSkip }) => {
|
|
26
22
|
const { setStep } = useAuthFormContext();
|
|
@@ -89,3 +85,51 @@ export const SetupStep: React.FC<SetupStepProps> = ({ onComplete, onSkip }) => {
|
|
|
89
85
|
// Fallback to loading
|
|
90
86
|
return <SetupLoading />;
|
|
91
87
|
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* SetupStepStandalone — same flow as SetupStep but without AuthFormContext.
|
|
91
|
+
* Use this inside ProfileLayout or any page outside AuthLayout.
|
|
92
|
+
*/
|
|
93
|
+
export const SetupStepStandalone: React.FC<SetupStepProps> = ({ onComplete, onSkip }) => {
|
|
94
|
+
const {
|
|
95
|
+
isLoading,
|
|
96
|
+
error,
|
|
97
|
+
setupData,
|
|
98
|
+
backupCodes,
|
|
99
|
+
backupCodesWarning,
|
|
100
|
+
setupStep,
|
|
101
|
+
startSetup,
|
|
102
|
+
confirmSetup,
|
|
103
|
+
} = useTwoFactorSetup({ onComplete, onError: () => {} });
|
|
104
|
+
|
|
105
|
+
React.useEffect(() => {
|
|
106
|
+
if (setupStep === 'idle') startSetup();
|
|
107
|
+
}, [setupStep, startSetup]);
|
|
108
|
+
|
|
109
|
+
if (isLoading && !setupData) return <SetupLoading />;
|
|
110
|
+
|
|
111
|
+
if (setupStep === 'complete' && backupCodes) {
|
|
112
|
+
return (
|
|
113
|
+
<SetupComplete
|
|
114
|
+
backupCodes={backupCodes}
|
|
115
|
+
backupCodesWarning={backupCodesWarning}
|
|
116
|
+
onDone={() => onComplete?.()}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (setupData) {
|
|
122
|
+
return (
|
|
123
|
+
<SetupQRCode
|
|
124
|
+
provisioningUri={setupData.provisioningUri}
|
|
125
|
+
secret={setupData.secret}
|
|
126
|
+
isLoading={isLoading}
|
|
127
|
+
error={error}
|
|
128
|
+
onConfirm={confirmSetup}
|
|
129
|
+
onSkip={onSkip}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return <SetupLoading />;
|
|
135
|
+
};
|