@payez/next-mvp 4.8.1 → 4.8.2
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/POST-INSTALL.md +408 -408
- package/README.md +859 -859
- package/dist/components/account/MobileNavDrawer.js +18 -18
- package/dist/components/account/UserAvatarMenu.js +1 -1
- package/dist/components/admin/AlertSettingsTab.js +40 -40
- package/dist/components/admin/LoggingSettingsTab.js +28 -28
- package/dist/lib/session-store.js +21 -21
- package/dist/lib/startup-init.js +19 -19
- package/dist/pages/login/page.js +28 -28
- package/dist/pages/settings/EnhancedSettingsPage.js +17 -17
- package/dist/vibe/sessions.js +28 -3
- package/package.json +2 -2
- package/scripts/check-publish-eol.mjs +33 -15
- package/src/api/auth-handler.ts +550 -550
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/change-password.ts +170 -170
- package/src/api-handlers/account/masked-info.ts +46 -46
- package/src/api-handlers/account/profile.ts +99 -99
- package/src/api-handlers/account/recovery/forward.ts +117 -117
- package/src/api-handlers/account/recovery/initiate.ts +59 -59
- package/src/api-handlers/account/recovery/send-code.ts +21 -21
- package/src/api-handlers/account/recovery/verify-code.ts +20 -20
- package/src/api-handlers/account/reset-password.ts +87 -87
- package/src/api-handlers/account/send-code.ts +70 -70
- package/src/api-handlers/account/update-phone.ts +84 -84
- package/src/api-handlers/account/validate-password.ts +139 -139
- package/src/api-handlers/account/verify-email.ts +134 -134
- package/src/api-handlers/account/verify-sms.ts +134 -134
- package/src/api-handlers/admin/analytics.ts +452 -452
- package/src/api-handlers/admin/audit.ts +232 -232
- package/src/api-handlers/admin/index.ts +63 -63
- package/src/api-handlers/admin/redis-sessions.ts +259 -259
- package/src/api-handlers/admin/sessions.ts +327 -327
- package/src/api-handlers/admin/site-logs.ts +388 -388
- package/src/api-handlers/admin/stats.ts +260 -260
- package/src/api-handlers/admin/users.ts +251 -251
- package/src/api-handlers/admin/vibe-data.ts +333 -333
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/credential-login-email-code.ts +92 -92
- package/src/api-handlers/auth/credential-login.ts +948 -948
- package/src/api-handlers/auth/jwks.ts +31 -31
- package/src/api-handlers/auth/login.ts +252 -252
- package/src/api-handlers/auth/passkey-signin.ts +116 -116
- package/src/api-handlers/auth/refresh.ts +829 -829
- package/src/api-handlers/auth/signout.ts +201 -201
- package/src/api-handlers/auth/status.ts +23 -23
- package/src/api-handlers/auth/update-session.ts +144 -144
- package/src/api-handlers/auth/validate.ts +45 -45
- package/src/api-handlers/auth/verify-code.ts +116 -116
- package/src/api-handlers/mfa/backup-code-verify.ts +170 -170
- package/src/api-handlers/mfa/backup-codes.ts +42 -42
- package/src/api-handlers/mfa/methods.ts +47 -47
- package/src/api-handlers/mfa/totp-confirm.ts +43 -43
- package/src/api-handlers/mfa/totp-enroll.ts +43 -43
- package/src/api-handlers/mfa/totp-verify.ts +172 -172
- package/src/api-handlers/oauth/resume.ts +182 -182
- package/src/api-handlers/onboarding/send-email-code.ts +18 -18
- package/src/api-handlers/onboarding/set-password.ts +21 -21
- package/src/api-handlers/onboarding/verify-code.ts +20 -20
- package/src/api-handlers/onboarding/verify-welcome-email.ts +17 -17
- package/src/api-handlers/passkeys/list.ts +32 -32
- package/src/api-handlers/passkeys/manage.ts +55 -55
- package/src/api-handlers/passkeys/register-options.ts +43 -43
- package/src/api-handlers/passkeys/register-verify.ts +42 -42
- package/src/api-handlers/session/refresh-viability.ts +56 -56
- package/src/api-handlers/session/viability.ts +238 -238
- package/src/api-handlers/test/force-expire.ts +167 -167
- package/src/auth/auth-decision.ts +292 -292
- package/src/auth/better-auth.ts +1688 -1688
- package/src/auth/idp-oauth.ts +310 -310
- package/src/auth/password-reset-bridge.ts +98 -98
- package/src/auth/revocation-fields.ts +16 -16
- package/src/auth/route-config.ts +236 -236
- package/src/auth/safe-callback.ts +199 -199
- package/src/auth/session-cookie-clear.ts +30 -30
- package/src/auth/session-reset-link.ts +159 -159
- package/src/auth/session-response-sanitizer.ts +151 -151
- package/src/auth/types/auth-types.ts +555 -555
- package/src/auth/types/index.ts +7 -7
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/auth/utils/idp-client.ts +364 -364
- package/src/auth/utils/index.ts +6 -6
- package/src/auth/utils/token-utils.ts +243 -243
- package/src/client/AuthContext.tsx +192 -192
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetch-with-auth.ts +47 -47
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/SessionSync.tsx +138 -138
- package/src/components/SignalRHealthCheck.tsx +131 -131
- package/src/components/account/MobileNavDrawer.tsx +305 -305
- package/src/components/account/UserAvatarMenu.tsx +277 -277
- package/src/components/account/index.ts +13 -13
- package/src/components/admin/AlertSettingsTab.tsx +728 -728
- package/src/components/admin/AnalyticsTab.tsx +703 -703
- package/src/components/admin/DataBrowserTab.tsx +508 -508
- package/src/components/admin/LoggingSettingsTab.tsx +665 -665
- package/src/components/admin/SessionsTab.tsx +421 -421
- package/src/components/admin/StatsTab.tsx +379 -379
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/components/admin/VibeAdminLayout.tsx +187 -187
- package/src/components/admin/index.ts +59 -59
- package/src/components/auth/CredentialSignInForm.tsx +240 -240
- package/src/components/auth/FederatedAuthSection.tsx +129 -129
- package/src/components/auth/ModeAwareLoginPage.tsx +146 -146
- package/src/components/auth/ModeAwareSignupPage.tsx +200 -200
- package/src/components/auth/PasswordResetBridge.tsx +30 -30
- package/src/components/auth/TraditionalAuthSection.tsx +103 -103
- package/src/components/recovery/CompleteStep.tsx +36 -36
- package/src/components/recovery/InitiateRecoveryStep.tsx +68 -68
- package/src/components/recovery/RequestedStep.tsx +30 -30
- package/src/components/recovery/SelectMethodStep.tsx +73 -73
- package/src/components/recovery/SetPasswordStep.tsx +118 -118
- package/src/components/recovery/VerifyCodeStep.tsx +90 -90
- package/src/components/reserved/ReservedRecoveryWarning.tsx +160 -160
- package/src/components/reserved/ReservedStatusBox.tsx +118 -118
- package/src/components/ui/BetaBadge.tsx +58 -58
- package/src/components/ui/Footer.tsx +93 -93
- package/src/config/env.ts +55 -55
- package/src/config/logger.ts +62 -62
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/config/vibe-log-transport.ts +264 -264
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useAuth.ts +117 -117
- package/src/hooks/useAuthSettings.ts +109 -109
- package/src/hooks/useAvailableProviders.ts +143 -143
- package/src/hooks/usePasswordValidation.ts +127 -127
- package/src/hooks/useProfile.ts +82 -82
- package/src/hooks/usePublicAuthSettings.ts +171 -171
- package/src/hooks/useSessionExpiration.ts +101 -101
- package/src/hooks/useViabilitySession.ts +337 -337
- package/src/index.ts +64 -64
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/api-handler.ts +643 -643
- package/src/lib/app-slug.ts +228 -228
- package/src/lib/authorize-resume.ts +172 -172
- package/src/lib/bearer-fetch.ts +31 -31
- package/src/lib/client-key.ts +88 -88
- package/src/lib/code-step-copy.ts +83 -83
- package/src/lib/credential-sign-in-client.ts +101 -101
- package/src/lib/demo-mode.ts +18 -18
- package/src/lib/device-id.ts +67 -67
- package/src/lib/ensure-fresh-access-token.ts +509 -509
- package/src/lib/geolocation.ts +265 -265
- package/src/lib/idp-client-config.ts +1005 -1005
- package/src/lib/idp-fetch.ts +169 -169
- package/src/lib/idp-url-guard.ts +37 -37
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/jwt-decode.ts +82 -82
- package/src/lib/lock-window.ts +49 -49
- package/src/lib/login-error-message.ts +41 -41
- package/src/lib/never-in-response.ts +64 -64
- package/src/lib/nextauth-secret.ts +168 -168
- package/src/lib/onboarding-proxy.ts +57 -57
- package/src/lib/password-rules.ts +50 -50
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +105 -105
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/roles.ts +177 -177
- package/src/lib/safe-error-response.ts +149 -149
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-cookie-shape.ts +185 -185
- package/src/lib/session-store.ts +1092 -1092
- package/src/lib/session.ts +34 -34
- package/src/lib/single-flight.ts +64 -64
- package/src/lib/site-logger.ts +245 -245
- package/src/lib/standardized-client-api.ts +901 -901
- package/src/lib/startup-init.ts +276 -276
- package/src/lib/test-aware-get-token.ts +41 -41
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +546 -546
- package/src/lib/token-log-id.ts +69 -69
- package/src/lib/twofa-rejection.ts +91 -91
- package/src/lib/types/api-responses.ts +336 -336
- package/src/lib/user-agent-parser.ts +252 -252
- package/src/logging/api/admin-analytics.ts +51 -51
- package/src/logging/api/audit-log.ts +53 -53
- package/src/logging/components/AdminAnalyticsLayout.tsx +49 -49
- package/src/logging/components/AuditLogViewer.tsx +125 -125
- package/src/logging/components/ErrorMetricsCard.tsx +98 -98
- package/src/logging/components/HealthMetricsCard.tsx +70 -70
- package/src/logging/hooks/useAdminAnalytics.ts +22 -22
- package/src/logging/hooks/useAuditLog.ts +24 -24
- package/src/logging/hooks/useErrorMetrics.ts +40 -40
- package/src/logging/hooks/useHealthMetrics.ts +44 -44
- package/src/logging/index.ts +18 -18
- package/src/logging/types/analytics.ts +81 -81
- package/src/logging/types/audit.ts +31 -31
- package/src/logging/types/index.ts +3 -3
- package/src/middleware/create-middleware.ts +1141 -1141
- package/src/middleware/rbac-check.ts +279 -279
- package/src/middleware/twofa-presets.ts +224 -224
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +331 -331
- package/src/pages/admin-login/page.tsx +111 -111
- package/src/pages/admin-page-permissions/PagePermissionsAdminPage.tsx +527 -527
- package/src/pages/admin-page-permissions/index.ts +7 -7
- package/src/pages/admin-roles/RolesAdminPage.tsx +533 -533
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/admin-roles/modals.tsx +469 -469
- package/src/pages/client-admin/ClientSiteAdminPage.tsx +382 -382
- package/src/pages/client-admin/index.ts +33 -33
- package/src/pages/coming-soon/page.tsx +83 -83
- package/src/pages/login/page.tsx +338 -338
- package/src/pages/onboarding/index.ts +11 -11
- package/src/pages/onboarding/page.tsx +212 -212
- package/src/pages/profile/EnhancedProfilePage.tsx +514 -514
- package/src/pages/profile/index.ts +9 -9
- package/src/pages/profile/page.tsx +166 -166
- package/src/pages/recovery/page.tsx +267 -267
- package/src/pages/roles/MyRolesPage.tsx +211 -211
- package/src/pages/roles/components.tsx +294 -294
- package/src/pages/roles/index.ts +17 -17
- package/src/pages/security/EnhancedSecurityPage.tsx +660 -660
- package/src/pages/security/index.ts +12 -12
- package/src/pages/security/page.tsx +507 -507
- package/src/pages/service-unavailable/page.tsx +63 -63
- package/src/pages/settings/EnhancedSettingsPage.tsx +642 -642
- package/src/pages/settings/index.ts +9 -9
- package/src/pages/settings/page.tsx +47 -47
- package/src/pages/showcase/ShowcasePage.tsx +532 -532
- package/src/pages/showcase/index.ts +13 -13
- package/src/pages/test-env/EmergencyLogoutPage.tsx +192 -192
- package/src/pages/test-env/JwtInspectPage.tsx +449 -449
- package/src/pages/test-env/RefreshTokenPage.tsx +178 -178
- package/src/pages/test-env/TestEnvPage.tsx +130 -130
- package/src/pages/test-env/guarded.tsx +81 -81
- package/src/pages/test-env/index.ts +77 -77
- package/src/pages/test-env/redact-credentials.ts +96 -96
- package/src/pages/verify-code/page.tsx +746 -746
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/update-phone.ts +13 -13
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/credential-login-email-code.ts +6 -6
- package/src/routes/auth/credential-login.ts +6 -6
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/logout.ts +111 -111
- package/src/routes/auth/nextauth.ts +45 -45
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +77 -77
- package/src/routes/auth/session-reset.ts +37 -37
- package/src/routes/auth/session.ts +261 -261
- package/src/routes/auth/settings.ts +181 -181
- package/src/routes/auth/viability.ts +519 -519
- package/src/routes/index.ts +17 -17
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/onboarding/index.ts +15 -15
- package/src/routes/onboarding/send-email-code.ts +2 -2
- package/src/routes/onboarding/set-password.ts +2 -2
- package/src/routes/onboarding/verify-code.ts +2 -2
- package/src/routes/onboarding/verify-welcome-email.ts +2 -2
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth-guard.ts +203 -203
- package/src/server/auth.ts +486 -486
- package/src/server/decode-session.ts +309 -309
- package/src/server/slim-middleware.ts +117 -117
- package/src/server/verified-session-token.ts +103 -103
- package/src/server/with-auth.ts +93 -93
- package/src/services/signalrActivityService.ts +258 -258
- package/src/stores/authStore.ts +1900 -1900
- package/src/templates/instrumentation.ts +60 -60
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +48 -48
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +401 -401
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
- package/src/utils/logout.ts +30 -30
- package/src/vibe/client.ts +877 -877
- package/src/vibe/enterprise-auth.ts +208 -208
- package/src/vibe/errors.ts +209 -209
- package/src/vibe/generic.ts +339 -339
- package/src/vibe/hooks/index.ts +367 -367
- package/src/vibe/index.ts +133 -133
- package/src/vibe/sessions.ts +925 -899
- package/src/vibe/types.ts +577 -577
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PAY-2121: the onboarding surface.
|
|
3
|
-
*
|
|
4
|
-
* default - the /account-auth/onboarding page (mount from the scaffold at
|
|
5
|
-
* app/account-auth/onboarding/page.tsx).
|
|
6
|
-
* PasswordResetBridge - mount once on `/` to honour the IdP's post-verify
|
|
7
|
-
* `#password_reset_required=true` fragment.
|
|
8
|
-
*/
|
|
9
|
-
export { default } from './page';
|
|
10
|
-
export { default as OnboardingPage } from './page';
|
|
11
|
-
export { PasswordResetBridge } from '../../components/auth/PasswordResetBridge';
|
|
1
|
+
/**
|
|
2
|
+
* PAY-2121: the onboarding surface.
|
|
3
|
+
*
|
|
4
|
+
* default - the /account-auth/onboarding page (mount from the scaffold at
|
|
5
|
+
* app/account-auth/onboarding/page.tsx).
|
|
6
|
+
* PasswordResetBridge - mount once on `/` to honour the IdP's post-verify
|
|
7
|
+
* `#password_reset_required=true` fragment.
|
|
8
|
+
*/
|
|
9
|
+
export { default } from './page';
|
|
10
|
+
export { default as OnboardingPage } from './page';
|
|
11
|
+
export { PasswordResetBridge } from '../../components/auth/PasswordResetBridge';
|
|
@@ -1,212 +1,212 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { Suspense, useEffect, useState } from 'react';
|
|
4
|
-
import { useSearchParams } from 'next/navigation';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* PAY-2121: the /account-auth/onboarding landing page.
|
|
8
|
-
*
|
|
9
|
-
* The IdP's welcome/invite email links to
|
|
10
|
-
* {site}/account-auth/onboarding?client_id=<numeric>&onboard_token=<token>
|
|
11
|
-
* (OnboardingLinkService.BuildOnboardingLink, PayEz-Core). A fresh
|
|
12
|
-
* @payez/next-mvp app had NO page at that path, so the invitee 404'd at the
|
|
13
|
-
* developer's own site. This page mounts it.
|
|
14
|
-
*
|
|
15
|
-
* It is TOKEN-DRIVEN: the invitee has no account or session; the onboard_token
|
|
16
|
-
* IS the credential. The page forwards { onboard_token } to the kit's onboarding
|
|
17
|
-
* API (the /api/onboarding/* handlers), which proxy the IdP. Two steps:
|
|
18
|
-
* 1. verify-welcome-email -> the masked contact info the invitee recognises;
|
|
19
|
-
* 2. verify-code -> reset_token + email; then set-password -> onboarding done.
|
|
20
|
-
*
|
|
21
|
-
* MOUNTING (what README section 5 asks a consumer to write; the CLI scaffold
|
|
22
|
-
* writes it too):
|
|
23
|
-
* app/account-auth/onboarding/page.tsx:
|
|
24
|
-
* export { default } from '@payez/next-mvp/pages/onboarding';
|
|
25
|
-
*
|
|
26
|
-
* A missing onboard_token is a typed state, never a crash: it renders a plain
|
|
27
|
-
* "this link is missing its token" with a way back to sign-in.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
type Step = 'code' | 'password' | 'done';
|
|
31
|
-
|
|
32
|
-
const H = { primary: 'var(--text-primary)', secondary: 'var(--text-secondary)', muted: 'var(--text-muted)' } as const;
|
|
33
|
-
|
|
34
|
-
function OnboardingContent() {
|
|
35
|
-
const params = useSearchParams();
|
|
36
|
-
const onboardToken = params?.get('onboard_token') ?? '';
|
|
37
|
-
|
|
38
|
-
const [step, setStep] = useState<Step>('code');
|
|
39
|
-
const [maskedEmail, setMaskedEmail] = useState<string | null>(null);
|
|
40
|
-
const [digits, setDigits] = useState('');
|
|
41
|
-
const [password, setPassword] = useState('');
|
|
42
|
-
const [confirm, setConfirm] = useState('');
|
|
43
|
-
const [resetToken, setResetToken] = useState('');
|
|
44
|
-
const [email, setEmail] = useState('');
|
|
45
|
-
const [busy, setBusy] = useState(false);
|
|
46
|
-
const [error, setError] = useState<string | null>(null);
|
|
47
|
-
|
|
48
|
-
// Step 1: tell the invitee which address the code went to (masked). Best
|
|
49
|
-
// effort - a failure here does not block entering a code they were emailed.
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
if (!onboardToken) return;
|
|
52
|
-
let alive = true;
|
|
53
|
-
(async () => {
|
|
54
|
-
try {
|
|
55
|
-
const res = await fetch('/api/onboarding/verify-welcome-email', {
|
|
56
|
-
method: 'POST',
|
|
57
|
-
headers: { 'Content-Type': 'application/json' },
|
|
58
|
-
body: JSON.stringify({ onboard_token: onboardToken }),
|
|
59
|
-
});
|
|
60
|
-
const body = await res.json().catch(() => null);
|
|
61
|
-
const data = body?.data?.data || body?.data || {};
|
|
62
|
-
const masked = data?.masked_user_info?.masked_email || data?.masked_email;
|
|
63
|
-
if (alive && typeof masked === 'string' && masked) setMaskedEmail(masked);
|
|
64
|
-
} catch { /* non-fatal */ }
|
|
65
|
-
})();
|
|
66
|
-
return () => { alive = false; };
|
|
67
|
-
}, [onboardToken]);
|
|
68
|
-
|
|
69
|
-
if (!onboardToken) {
|
|
70
|
-
return (
|
|
71
|
-
<Shell>
|
|
72
|
-
<h1 style={titleStyle}>This invitation link is missing its token.</h1>
|
|
73
|
-
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>
|
|
74
|
-
The link may have been truncated or copied incompletely. Ask for a new invitation, or sign in if you already have an
|
|
75
|
-
account.
|
|
76
|
-
</p>
|
|
77
|
-
<p style={{ marginTop: 16 }}>
|
|
78
|
-
<a href="/account-auth/login" style={linkStyle}>Go to sign in</a>
|
|
79
|
-
</p>
|
|
80
|
-
</Shell>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function submitCode(e: React.FormEvent) {
|
|
85
|
-
e.preventDefault();
|
|
86
|
-
setBusy(true);
|
|
87
|
-
setError(null);
|
|
88
|
-
try {
|
|
89
|
-
const res = await fetch('/api/onboarding/verify-code', {
|
|
90
|
-
method: 'POST',
|
|
91
|
-
headers: { 'Content-Type': 'application/json' },
|
|
92
|
-
body: JSON.stringify({ onboard_token: onboardToken, code: digits }),
|
|
93
|
-
});
|
|
94
|
-
const body = await res.json().catch(() => null);
|
|
95
|
-
const data = body?.data?.data || body?.data || body;
|
|
96
|
-
if (!res.ok || !data?.reset_token || !data?.email) throw new Error();
|
|
97
|
-
setResetToken(data.reset_token);
|
|
98
|
-
setEmail(data.email);
|
|
99
|
-
setStep('password');
|
|
100
|
-
} catch {
|
|
101
|
-
setError('That code was not accepted. Check it and try again.');
|
|
102
|
-
} finally {
|
|
103
|
-
setBusy(false);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async function submitPassword(e: React.FormEvent) {
|
|
108
|
-
e.preventDefault();
|
|
109
|
-
if (password !== confirm) { setError('The two passwords do not match.'); return; }
|
|
110
|
-
setBusy(true);
|
|
111
|
-
setError(null);
|
|
112
|
-
try {
|
|
113
|
-
const res = await fetch('/api/onboarding/set-password', {
|
|
114
|
-
method: 'POST',
|
|
115
|
-
headers: { 'Content-Type': 'application/json' },
|
|
116
|
-
body: JSON.stringify({ onboard_token: onboardToken, email, reset_token: resetToken, new_password: password }),
|
|
117
|
-
});
|
|
118
|
-
const body = await res.json().catch(() => null);
|
|
119
|
-
if (!res.ok || (body && body.success === false)) throw new Error(body?.message || '');
|
|
120
|
-
setStep('done');
|
|
121
|
-
} catch (err) {
|
|
122
|
-
setError(err instanceof Error && err.message ? err.message : 'We could not set that password. Try again.');
|
|
123
|
-
} finally {
|
|
124
|
-
setBusy(false);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<Shell>
|
|
130
|
-
{step === 'code' && (
|
|
131
|
-
<>
|
|
132
|
-
<h1 style={titleStyle}>Finish setting up your account</h1>
|
|
133
|
-
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>
|
|
134
|
-
{maskedEmail
|
|
135
|
-
? `We emailed a 6-digit code to ${maskedEmail}. Enter it below.`
|
|
136
|
-
: 'Enter the 6-digit code from your invitation email.'}
|
|
137
|
-
</p>
|
|
138
|
-
<form onSubmit={submitCode} noValidate style={{ marginTop: 16 }}>
|
|
139
|
-
<input
|
|
140
|
-
value={digits}
|
|
141
|
-
onChange={(e) => setDigits(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
142
|
-
inputMode="numeric"
|
|
143
|
-
autoComplete="one-time-code"
|
|
144
|
-
aria-label="6-digit code"
|
|
145
|
-
placeholder="000000"
|
|
146
|
-
style={{ ...inputStyle, letterSpacing: 6, textAlign: 'center', fontFamily: 'monospace', fontSize: 20 }}
|
|
147
|
-
/>
|
|
148
|
-
<button type="submit" disabled={busy || digits.length < 6} style={ctaStyle(busy || digits.length < 6)}>
|
|
149
|
-
{busy ? 'Verifying…' : 'Verify'}
|
|
150
|
-
</button>
|
|
151
|
-
</form>
|
|
152
|
-
</>
|
|
153
|
-
)}
|
|
154
|
-
|
|
155
|
-
{step === 'password' && (
|
|
156
|
-
<>
|
|
157
|
-
<h1 style={titleStyle}>Choose a password</h1>
|
|
158
|
-
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>Set the password you will sign in with.</p>
|
|
159
|
-
<form onSubmit={submitPassword} noValidate style={{ marginTop: 16 }}>
|
|
160
|
-
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} aria-label="New password" placeholder="New password" style={inputStyle} />
|
|
161
|
-
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} aria-label="Confirm password" placeholder="Confirm password" style={{ ...inputStyle, marginTop: 10 }} />
|
|
162
|
-
<button type="submit" disabled={busy || !password || !confirm} style={ctaStyle(busy || !password || !confirm)}>
|
|
163
|
-
{busy ? 'Saving…' : 'Set password'}
|
|
164
|
-
</button>
|
|
165
|
-
</form>
|
|
166
|
-
</>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
{step === 'done' && (
|
|
170
|
-
<>
|
|
171
|
-
<h1 style={titleStyle}>You're all set.</h1>
|
|
172
|
-
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>Your account is ready. You can sign in now.</p>
|
|
173
|
-
<p style={{ marginTop: 16 }}>
|
|
174
|
-
<a href="/account-auth/login" style={linkStyle}>Go to sign in</a>
|
|
175
|
-
</p>
|
|
176
|
-
</>
|
|
177
|
-
)}
|
|
178
|
-
|
|
179
|
-
{error && <p style={{ color: '#b00020', fontSize: 13, marginTop: 12 }}>{error}</p>}
|
|
180
|
-
</Shell>
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const titleStyle: React.CSSProperties = { color: H.primary, fontSize: 20, fontWeight: 600, margin: 0 };
|
|
185
|
-
const linkStyle: React.CSSProperties = { color: H.primary, fontWeight: 600, textDecoration: 'underline' };
|
|
186
|
-
const inputStyle: React.CSSProperties = {
|
|
187
|
-
width: '100%', padding: '10px 12px', border: '1px solid var(--border-default, #d1d5db)',
|
|
188
|
-
borderRadius: 10, color: H.primary, fontSize: 15, background: 'var(--bg-card, #fff)',
|
|
189
|
-
};
|
|
190
|
-
const ctaStyle = (disabled: boolean): React.CSSProperties => ({
|
|
191
|
-
marginTop: 14, width: '100%', padding: '10px 12px', borderRadius: 10, border: 'none',
|
|
192
|
-
background: H.primary, color: '#fff', fontWeight: 600, fontSize: 15, cursor: disabled ? 'default' : 'pointer',
|
|
193
|
-
opacity: disabled ? 0.5 : 1,
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
function Shell({ children }: { children: React.ReactNode }) {
|
|
197
|
-
return (
|
|
198
|
-
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, background: 'var(--bg-default, #f9fafb)' }}>
|
|
199
|
-
<div style={{ width: '100%', maxWidth: 420, background: 'var(--bg-card, #fff)', border: '1px solid var(--border-default, #e5e7eb)', borderRadius: 16, padding: 28 }}>
|
|
200
|
-
{children}
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export default function OnboardingPage() {
|
|
207
|
-
return (
|
|
208
|
-
<Suspense fallback={<div style={{ minHeight: '100vh' }} />}>
|
|
209
|
-
<OnboardingContent />
|
|
210
|
-
</Suspense>
|
|
211
|
-
);
|
|
212
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { Suspense, useEffect, useState } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* PAY-2121: the /account-auth/onboarding landing page.
|
|
8
|
+
*
|
|
9
|
+
* The IdP's welcome/invite email links to
|
|
10
|
+
* {site}/account-auth/onboarding?client_id=<numeric>&onboard_token=<token>
|
|
11
|
+
* (OnboardingLinkService.BuildOnboardingLink, PayEz-Core). A fresh
|
|
12
|
+
* @payez/next-mvp app had NO page at that path, so the invitee 404'd at the
|
|
13
|
+
* developer's own site. This page mounts it.
|
|
14
|
+
*
|
|
15
|
+
* It is TOKEN-DRIVEN: the invitee has no account or session; the onboard_token
|
|
16
|
+
* IS the credential. The page forwards { onboard_token } to the kit's onboarding
|
|
17
|
+
* API (the /api/onboarding/* handlers), which proxy the IdP. Two steps:
|
|
18
|
+
* 1. verify-welcome-email -> the masked contact info the invitee recognises;
|
|
19
|
+
* 2. verify-code -> reset_token + email; then set-password -> onboarding done.
|
|
20
|
+
*
|
|
21
|
+
* MOUNTING (what README section 5 asks a consumer to write; the CLI scaffold
|
|
22
|
+
* writes it too):
|
|
23
|
+
* app/account-auth/onboarding/page.tsx:
|
|
24
|
+
* export { default } from '@payez/next-mvp/pages/onboarding';
|
|
25
|
+
*
|
|
26
|
+
* A missing onboard_token is a typed state, never a crash: it renders a plain
|
|
27
|
+
* "this link is missing its token" with a way back to sign-in.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
type Step = 'code' | 'password' | 'done';
|
|
31
|
+
|
|
32
|
+
const H = { primary: 'var(--text-primary)', secondary: 'var(--text-secondary)', muted: 'var(--text-muted)' } as const;
|
|
33
|
+
|
|
34
|
+
function OnboardingContent() {
|
|
35
|
+
const params = useSearchParams();
|
|
36
|
+
const onboardToken = params?.get('onboard_token') ?? '';
|
|
37
|
+
|
|
38
|
+
const [step, setStep] = useState<Step>('code');
|
|
39
|
+
const [maskedEmail, setMaskedEmail] = useState<string | null>(null);
|
|
40
|
+
const [digits, setDigits] = useState('');
|
|
41
|
+
const [password, setPassword] = useState('');
|
|
42
|
+
const [confirm, setConfirm] = useState('');
|
|
43
|
+
const [resetToken, setResetToken] = useState('');
|
|
44
|
+
const [email, setEmail] = useState('');
|
|
45
|
+
const [busy, setBusy] = useState(false);
|
|
46
|
+
const [error, setError] = useState<string | null>(null);
|
|
47
|
+
|
|
48
|
+
// Step 1: tell the invitee which address the code went to (masked). Best
|
|
49
|
+
// effort - a failure here does not block entering a code they were emailed.
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!onboardToken) return;
|
|
52
|
+
let alive = true;
|
|
53
|
+
(async () => {
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetch('/api/onboarding/verify-welcome-email', {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
body: JSON.stringify({ onboard_token: onboardToken }),
|
|
59
|
+
});
|
|
60
|
+
const body = await res.json().catch(() => null);
|
|
61
|
+
const data = body?.data?.data || body?.data || {};
|
|
62
|
+
const masked = data?.masked_user_info?.masked_email || data?.masked_email;
|
|
63
|
+
if (alive && typeof masked === 'string' && masked) setMaskedEmail(masked);
|
|
64
|
+
} catch { /* non-fatal */ }
|
|
65
|
+
})();
|
|
66
|
+
return () => { alive = false; };
|
|
67
|
+
}, [onboardToken]);
|
|
68
|
+
|
|
69
|
+
if (!onboardToken) {
|
|
70
|
+
return (
|
|
71
|
+
<Shell>
|
|
72
|
+
<h1 style={titleStyle}>This invitation link is missing its token.</h1>
|
|
73
|
+
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>
|
|
74
|
+
The link may have been truncated or copied incompletely. Ask for a new invitation, or sign in if you already have an
|
|
75
|
+
account.
|
|
76
|
+
</p>
|
|
77
|
+
<p style={{ marginTop: 16 }}>
|
|
78
|
+
<a href="/account-auth/login" style={linkStyle}>Go to sign in</a>
|
|
79
|
+
</p>
|
|
80
|
+
</Shell>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function submitCode(e: React.FormEvent) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
setBusy(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
try {
|
|
89
|
+
const res = await fetch('/api/onboarding/verify-code', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({ onboard_token: onboardToken, code: digits }),
|
|
93
|
+
});
|
|
94
|
+
const body = await res.json().catch(() => null);
|
|
95
|
+
const data = body?.data?.data || body?.data || body;
|
|
96
|
+
if (!res.ok || !data?.reset_token || !data?.email) throw new Error();
|
|
97
|
+
setResetToken(data.reset_token);
|
|
98
|
+
setEmail(data.email);
|
|
99
|
+
setStep('password');
|
|
100
|
+
} catch {
|
|
101
|
+
setError('That code was not accepted. Check it and try again.');
|
|
102
|
+
} finally {
|
|
103
|
+
setBusy(false);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function submitPassword(e: React.FormEvent) {
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
if (password !== confirm) { setError('The two passwords do not match.'); return; }
|
|
110
|
+
setBusy(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
try {
|
|
113
|
+
const res = await fetch('/api/onboarding/set-password', {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers: { 'Content-Type': 'application/json' },
|
|
116
|
+
body: JSON.stringify({ onboard_token: onboardToken, email, reset_token: resetToken, new_password: password }),
|
|
117
|
+
});
|
|
118
|
+
const body = await res.json().catch(() => null);
|
|
119
|
+
if (!res.ok || (body && body.success === false)) throw new Error(body?.message || '');
|
|
120
|
+
setStep('done');
|
|
121
|
+
} catch (err) {
|
|
122
|
+
setError(err instanceof Error && err.message ? err.message : 'We could not set that password. Try again.');
|
|
123
|
+
} finally {
|
|
124
|
+
setBusy(false);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Shell>
|
|
130
|
+
{step === 'code' && (
|
|
131
|
+
<>
|
|
132
|
+
<h1 style={titleStyle}>Finish setting up your account</h1>
|
|
133
|
+
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>
|
|
134
|
+
{maskedEmail
|
|
135
|
+
? `We emailed a 6-digit code to ${maskedEmail}. Enter it below.`
|
|
136
|
+
: 'Enter the 6-digit code from your invitation email.'}
|
|
137
|
+
</p>
|
|
138
|
+
<form onSubmit={submitCode} noValidate style={{ marginTop: 16 }}>
|
|
139
|
+
<input
|
|
140
|
+
value={digits}
|
|
141
|
+
onChange={(e) => setDigits(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
142
|
+
inputMode="numeric"
|
|
143
|
+
autoComplete="one-time-code"
|
|
144
|
+
aria-label="6-digit code"
|
|
145
|
+
placeholder="000000"
|
|
146
|
+
style={{ ...inputStyle, letterSpacing: 6, textAlign: 'center', fontFamily: 'monospace', fontSize: 20 }}
|
|
147
|
+
/>
|
|
148
|
+
<button type="submit" disabled={busy || digits.length < 6} style={ctaStyle(busy || digits.length < 6)}>
|
|
149
|
+
{busy ? 'Verifying…' : 'Verify'}
|
|
150
|
+
</button>
|
|
151
|
+
</form>
|
|
152
|
+
</>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{step === 'password' && (
|
|
156
|
+
<>
|
|
157
|
+
<h1 style={titleStyle}>Choose a password</h1>
|
|
158
|
+
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>Set the password you will sign in with.</p>
|
|
159
|
+
<form onSubmit={submitPassword} noValidate style={{ marginTop: 16 }}>
|
|
160
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} aria-label="New password" placeholder="New password" style={inputStyle} />
|
|
161
|
+
<input type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} aria-label="Confirm password" placeholder="Confirm password" style={{ ...inputStyle, marginTop: 10 }} />
|
|
162
|
+
<button type="submit" disabled={busy || !password || !confirm} style={ctaStyle(busy || !password || !confirm)}>
|
|
163
|
+
{busy ? 'Saving…' : 'Set password'}
|
|
164
|
+
</button>
|
|
165
|
+
</form>
|
|
166
|
+
</>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{step === 'done' && (
|
|
170
|
+
<>
|
|
171
|
+
<h1 style={titleStyle}>You're all set.</h1>
|
|
172
|
+
<p style={{ color: H.secondary, fontSize: 14, marginTop: 8 }}>Your account is ready. You can sign in now.</p>
|
|
173
|
+
<p style={{ marginTop: 16 }}>
|
|
174
|
+
<a href="/account-auth/login" style={linkStyle}>Go to sign in</a>
|
|
175
|
+
</p>
|
|
176
|
+
</>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
{error && <p style={{ color: '#b00020', fontSize: 13, marginTop: 12 }}>{error}</p>}
|
|
180
|
+
</Shell>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const titleStyle: React.CSSProperties = { color: H.primary, fontSize: 20, fontWeight: 600, margin: 0 };
|
|
185
|
+
const linkStyle: React.CSSProperties = { color: H.primary, fontWeight: 600, textDecoration: 'underline' };
|
|
186
|
+
const inputStyle: React.CSSProperties = {
|
|
187
|
+
width: '100%', padding: '10px 12px', border: '1px solid var(--border-default, #d1d5db)',
|
|
188
|
+
borderRadius: 10, color: H.primary, fontSize: 15, background: 'var(--bg-card, #fff)',
|
|
189
|
+
};
|
|
190
|
+
const ctaStyle = (disabled: boolean): React.CSSProperties => ({
|
|
191
|
+
marginTop: 14, width: '100%', padding: '10px 12px', borderRadius: 10, border: 'none',
|
|
192
|
+
background: H.primary, color: '#fff', fontWeight: 600, fontSize: 15, cursor: disabled ? 'default' : 'pointer',
|
|
193
|
+
opacity: disabled ? 0.5 : 1,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
function Shell({ children }: { children: React.ReactNode }) {
|
|
197
|
+
return (
|
|
198
|
+
<div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16, background: 'var(--bg-default, #f9fafb)' }}>
|
|
199
|
+
<div style={{ width: '100%', maxWidth: 420, background: 'var(--bg-card, #fff)', border: '1px solid var(--border-default, #e5e7eb)', borderRadius: 16, padding: 28 }}>
|
|
200
|
+
{children}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export default function OnboardingPage() {
|
|
207
|
+
return (
|
|
208
|
+
<Suspense fallback={<div style={{ minHeight: '100vh' }} />}>
|
|
209
|
+
<OnboardingContent />
|
|
210
|
+
</Suspense>
|
|
211
|
+
);
|
|
212
|
+
}
|