@shipfox/client-auth 0.2.0
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/.storybook/main.ts +1 -0
- package/.storybook/preview.css +7 -0
- package/.storybook/preview.tsx +56 -0
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +111 -0
- package/LICENSE +21 -0
- package/dist/components/auth-guard.d.ts +5 -0
- package/dist/components/auth-guard.d.ts.map +1 -0
- package/dist/components/auth-guard.js +61 -0
- package/dist/components/auth-guard.js.map +1 -0
- package/dist/components/auth-provider.d.ts +11 -0
- package/dist/components/auth-provider.d.ts.map +1 -0
- package/dist/components/auth-provider.js +21 -0
- package/dist/components/auth-provider.js.map +1 -0
- package/dist/components/auth-shell.d.ts +11 -0
- package/dist/components/auth-shell.d.ts.map +1 -0
- package/dist/components/auth-shell.js +63 -0
- package/dist/components/auth-shell.js.map +1 -0
- package/dist/components/redirect-target.d.ts +2 -0
- package/dist/components/redirect-target.d.ts.map +1 -0
- package/dist/components/redirect-target.js +22 -0
- package/dist/components/redirect-target.js.map +1 -0
- package/dist/components/workspace-crumb.d.ts +3 -0
- package/dist/components/workspace-crumb.d.ts.map +1 -0
- package/dist/components/workspace-crumb.js +3 -0
- package/dist/components/workspace-crumb.js.map +1 -0
- package/dist/components/workspace-switcher.d.ts +6 -0
- package/dist/components/workspace-switcher.d.ts.map +1 -0
- package/dist/components/workspace-switcher.js +91 -0
- package/dist/components/workspace-switcher.js.map +1 -0
- package/dist/components/workspace-switcher.stories.d.ts +26 -0
- package/dist/components/workspace-switcher.stories.d.ts.map +1 -0
- package/dist/components/workspace-switcher.stories.js +168 -0
- package/dist/components/workspace-switcher.stories.js.map +1 -0
- package/dist/feature.d.ts +33 -0
- package/dist/feature.d.ts.map +1 -0
- package/dist/feature.js +43 -0
- package/dist/feature.js.map +1 -0
- package/dist/hooks/api/login-auth.d.ts +29 -0
- package/dist/hooks/api/login-auth.d.ts.map +1 -0
- package/dist/hooks/api/login-auth.js +42 -0
- package/dist/hooks/api/login-auth.js.map +1 -0
- package/dist/hooks/api/logout-auth.d.ts +2 -0
- package/dist/hooks/api/logout-auth.d.ts.map +1 -0
- package/dist/hooks/api/logout-auth.js +32 -0
- package/dist/hooks/api/logout-auth.js.map +1 -0
- package/dist/hooks/api/password-reset-auth.d.ts +33 -0
- package/dist/hooks/api/password-reset-auth.d.ts.map +1 -0
- package/dist/hooks/api/password-reset-auth.js +48 -0
- package/dist/hooks/api/password-reset-auth.js.map +1 -0
- package/dist/hooks/api/refresh-auth.d.ts +2 -0
- package/dist/hooks/api/refresh-auth.d.ts.map +1 -0
- package/dist/hooks/api/refresh-auth.js +3 -0
- package/dist/hooks/api/refresh-auth.js.map +1 -0
- package/dist/hooks/api/signup-auth.d.ts +27 -0
- package/dist/hooks/api/signup-auth.d.ts.map +1 -0
- package/dist/hooks/api/signup-auth.js +15 -0
- package/dist/hooks/api/signup-auth.js.map +1 -0
- package/dist/hooks/api/verify-email-auth.d.ts +20 -0
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -0
- package/dist/hooks/api/verify-email-auth.js +48 -0
- package/dist/hooks/api/verify-email-auth.js.map +1 -0
- package/dist/hooks/api/workspace-auth.d.ts +22 -0
- package/dist/hooks/api/workspace-auth.d.ts.map +1 -0
- package/dist/hooks/api/workspace-auth.js +25 -0
- package/dist/hooks/api/workspace-auth.js.map +1 -0
- package/dist/hooks/index.d.ts +11 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +11 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-active-workspace.d.ts +2 -0
- package/dist/hooks/use-active-workspace.d.ts.map +1 -0
- package/dist/hooks/use-active-workspace.js +3 -0
- package/dist/hooks/use-active-workspace.js.map +1 -0
- package/dist/hooks/use-auth-state.d.ts +2 -0
- package/dist/hooks/use-auth-state.d.ts.map +1 -0
- package/dist/hooks/use-auth-state.js +3 -0
- package/dist/hooks/use-auth-state.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/pages/email-verification-resend-model.d.ts +7 -0
- package/dist/pages/email-verification-resend-model.d.ts.map +1 -0
- package/dist/pages/email-verification-resend-model.js +15 -0
- package/dist/pages/email-verification-resend-model.js.map +1 -0
- package/dist/pages/form-errors.d.ts +19 -0
- package/dist/pages/form-errors.d.ts.map +1 -0
- package/dist/pages/form-errors.js +51 -0
- package/dist/pages/form-errors.js.map +1 -0
- package/dist/pages/form-utils.d.ts +2 -0
- package/dist/pages/form-utils.d.ts.map +1 -0
- package/dist/pages/form-utils.js +28 -0
- package/dist/pages/form-utils.js.map +1 -0
- package/dist/pages/invitation-context.d.ts +31 -0
- package/dist/pages/invitation-context.d.ts.map +1 -0
- package/dist/pages/invitation-context.js +50 -0
- package/dist/pages/invitation-context.js.map +1 -0
- package/dist/pages/login-page.d.ts +2 -0
- package/dist/pages/login-page.d.ts.map +1 -0
- package/dist/pages/login-page.js +214 -0
- package/dist/pages/login-page.js.map +1 -0
- package/dist/pages/logout-page.d.ts +2 -0
- package/dist/pages/logout-page.d.ts.map +1 -0
- package/dist/pages/logout-page.js +45 -0
- package/dist/pages/logout-page.js.map +1 -0
- package/dist/pages/password-reset-page.d.ts +2 -0
- package/dist/pages/password-reset-page.d.ts.map +1 -0
- package/dist/pages/password-reset-page.js +265 -0
- package/dist/pages/password-reset-page.js.map +1 -0
- package/dist/pages/signup-page.d.ts +2 -0
- package/dist/pages/signup-page.d.ts.map +1 -0
- package/dist/pages/signup-page.js +384 -0
- package/dist/pages/signup-page.js.map +1 -0
- package/dist/pages/verify-email-page.d.ts +2 -0
- package/dist/pages/verify-email-page.d.ts.map +1 -0
- package/dist/pages/verify-email-page.js +57 -0
- package/dist/pages/verify-email-page.js.map +1 -0
- package/dist/pages/workspace-onboarding-page.d.ts +2 -0
- package/dist/pages/workspace-onboarding-page.d.ts.map +1 -0
- package/dist/pages/workspace-onboarding-page.js +294 -0
- package/dist/pages/workspace-onboarding-page.js.map +1 -0
- package/dist/routes/index.d.ts +10 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +29 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/login.d.ts +5 -0
- package/dist/routes/login.d.ts.map +1 -0
- package/dist/routes/login.js +11 -0
- package/dist/routes/login.js.map +1 -0
- package/dist/routes/logout.d.ts +6 -0
- package/dist/routes/logout.d.ts.map +1 -0
- package/dist/routes/logout.js +7 -0
- package/dist/routes/logout.js.map +1 -0
- package/dist/routes/reset.d.ts +5 -0
- package/dist/routes/reset.d.ts.map +1 -0
- package/dist/routes/reset.js +11 -0
- package/dist/routes/reset.js.map +1 -0
- package/dist/routes/signup.d.ts +5 -0
- package/dist/routes/signup.d.ts.map +1 -0
- package/dist/routes/signup.js +11 -0
- package/dist/routes/signup.js.map +1 -0
- package/dist/routes/verify-email.d.ts +5 -0
- package/dist/routes/verify-email.d.ts.map +1 -0
- package/dist/routes/verify-email.js +11 -0
- package/dist/routes/verify-email.js.map +1 -0
- package/dist/routes/workspace-onboarding.d.ts +13 -0
- package/dist/routes/workspace-onboarding.d.ts.map +1 -0
- package/dist/routes/workspace-onboarding.js +18 -0
- package/dist/routes/workspace-onboarding.js.map +1 -0
- package/dist/state/auth.d.ts +10 -0
- package/dist/state/auth.d.ts.map +1 -0
- package/dist/state/auth.js +9 -0
- package/dist/state/auth.js.map +1 -0
- package/dist/state/last-workspace.d.ts +2 -0
- package/dist/state/last-workspace.d.ts.map +1 -0
- package/dist/state/last-workspace.js +3 -0
- package/dist/state/last-workspace.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +116 -0
- package/src/components/auth-guard.tsx +58 -0
- package/src/components/auth-provider.test.tsx +164 -0
- package/src/components/auth-provider.tsx +24 -0
- package/src/components/auth-shell.tsx +53 -0
- package/src/components/redirect-target.test.ts +60 -0
- package/src/components/redirect-target.ts +21 -0
- package/src/components/workspace-crumb.tsx +2 -0
- package/src/components/workspace-switcher.stories.tsx +169 -0
- package/src/components/workspace-switcher.tsx +76 -0
- package/src/feature.ts +18 -0
- package/src/hooks/api/login-auth.test.ts +45 -0
- package/src/hooks/api/login-auth.ts +41 -0
- package/src/hooks/api/logout-auth.ts +26 -0
- package/src/hooks/api/password-reset-auth.test.ts +68 -0
- package/src/hooks/api/password-reset-auth.ts +51 -0
- package/src/hooks/api/refresh-auth.test.ts +37 -0
- package/src/hooks/api/refresh-auth.ts +5 -0
- package/src/hooks/api/signup-auth.ts +11 -0
- package/src/hooks/api/verify-email-auth.ts +55 -0
- package/src/hooks/api/workspace-auth.test.ts +40 -0
- package/src/hooks/api/workspace-auth.ts +25 -0
- package/src/hooks/index.ts +13 -0
- package/src/hooks/use-active-workspace.ts +1 -0
- package/src/hooks/use-auth-state.ts +1 -0
- package/src/index.ts +18 -0
- package/src/pages/email-verification-resend-model.test.ts +52 -0
- package/src/pages/email-verification-resend-model.ts +21 -0
- package/src/pages/form-errors.test.ts +114 -0
- package/src/pages/form-errors.ts +45 -0
- package/src/pages/form-utils.test.ts +37 -0
- package/src/pages/form-utils.ts +29 -0
- package/src/pages/invitation-context.ts +57 -0
- package/src/pages/login-page.test.tsx +128 -0
- package/src/pages/login-page.tsx +184 -0
- package/src/pages/logout-page.tsx +33 -0
- package/src/pages/password-reset-page.test.tsx +109 -0
- package/src/pages/password-reset-page.tsx +222 -0
- package/src/pages/signup-page.test.tsx +289 -0
- package/src/pages/signup-page.tsx +354 -0
- package/src/pages/verify-email-page.test.tsx +82 -0
- package/src/pages/verify-email-page.tsx +48 -0
- package/src/pages/workspace-onboarding-page.test.tsx +161 -0
- package/src/pages/workspace-onboarding-page.tsx +213 -0
- package/src/routes/index.tsx +18 -0
- package/src/routes/login.tsx +11 -0
- package/src/routes/logout.tsx +4 -0
- package/src/routes/reset.tsx +11 -0
- package/src/routes/signup.tsx +11 -0
- package/src/routes/verify-email.tsx +11 -0
- package/src/routes/workspace-onboarding.tsx +14 -0
- package/src/state/auth.ts +18 -0
- package/src/state/last-workspace.test.ts +63 -0
- package/src/state/last-workspace.ts +5 -0
- package/test/factories/user.ts +17 -0
- package/test/pages.tsx +83 -0
- package/test/setup.ts +3 -0
- package/test/utils.ts +11 -0
- package/tsconfig.build.json +10 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vercel.json +8 -0
- package/vitest.config.ts +71 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { signupBodySchema } from '@shipfox/api-auth-dto';
|
|
3
|
+
import { displayNameFieldError } from '@shipfox/client-ui';
|
|
4
|
+
import { Button, ButtonLink } from '@shipfox/react-ui/button';
|
|
5
|
+
import { Callout } from '@shipfox/react-ui/callout';
|
|
6
|
+
import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
|
|
7
|
+
import { Icon } from '@shipfox/react-ui/icon';
|
|
8
|
+
import { toast } from '@shipfox/react-ui/toast';
|
|
9
|
+
import { Text } from '@shipfox/react-ui/typography';
|
|
10
|
+
import { useForm } from '@tanstack/react-form';
|
|
11
|
+
import { Link, useNavigate, useSearch } from '@tanstack/react-router';
|
|
12
|
+
import { useAtom } from 'jotai';
|
|
13
|
+
import { useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { AuthShell } from '#/components/auth-shell.js';
|
|
15
|
+
import { useRefreshAuth } from '#hooks/api/refresh-auth.js';
|
|
16
|
+
import { useSignupAuth } from '#hooks/api/signup-auth.js';
|
|
17
|
+
import { useResendEmailVerificationAuth } from '#hooks/api/verify-email-auth.js';
|
|
18
|
+
import { authFormDraftAtom, initialAuthFormDraft } from '#state/auth.js';
|
|
19
|
+
import { getLocalResendAvailableAt, getResendRemainingSeconds, parseNextResendAvailableAt } from './email-verification-resend-model.js';
|
|
20
|
+
import { signupErrorToFormError } from './form-errors.js';
|
|
21
|
+
import { authErrorMessage } from './form-utils.js';
|
|
22
|
+
import { extractInvitationToken, pendingInvitation, useInvitationContext } from './invitation-context.js';
|
|
23
|
+
export function SignupPage() {
|
|
24
|
+
const signup = useSignupAuth();
|
|
25
|
+
const resendEmailVerification = useResendEmailVerificationAuth();
|
|
26
|
+
const refreshAuth = useRefreshAuth();
|
|
27
|
+
const navigate = useNavigate();
|
|
28
|
+
const search = useSearch({
|
|
29
|
+
strict: false
|
|
30
|
+
});
|
|
31
|
+
const invitationToken = extractInvitationToken(search.redirect);
|
|
32
|
+
const invitationPreview = useInvitationContext(invitationToken);
|
|
33
|
+
const invitationPending = pendingInvitation(invitationPreview.data);
|
|
34
|
+
const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
35
|
+
const [submittedEmail, setSubmittedEmail] = useState();
|
|
36
|
+
const [now, setNow] = useState(()=>Date.now());
|
|
37
|
+
const [nextResendAvailableAt, setNextResendAvailableAt] = useState();
|
|
38
|
+
const [formError, setFormError] = useState();
|
|
39
|
+
const [resendError, setResendError] = useState();
|
|
40
|
+
const draftRef = useRef(authFormDraft);
|
|
41
|
+
draftRef.current = authFormDraft;
|
|
42
|
+
// Set just before clearing the draft on success so the unmount cleanup
|
|
43
|
+
// below does not repersist the just-submitted credentials.
|
|
44
|
+
const skipDraftPersistRef = useRef(false);
|
|
45
|
+
const resendRemainingSeconds = getResendRemainingSeconds({
|
|
46
|
+
nextResendAvailableAt,
|
|
47
|
+
now
|
|
48
|
+
});
|
|
49
|
+
const isResendCoolingDown = resendRemainingSeconds > 0;
|
|
50
|
+
const form = useForm({
|
|
51
|
+
defaultValues: {
|
|
52
|
+
email: authFormDraft.email,
|
|
53
|
+
password: authFormDraft.password,
|
|
54
|
+
name: ''
|
|
55
|
+
},
|
|
56
|
+
onSubmit: async ({ value })=>{
|
|
57
|
+
setFormError(undefined);
|
|
58
|
+
try {
|
|
59
|
+
const body = signupBodySchema.parse({
|
|
60
|
+
email: value.email,
|
|
61
|
+
password: value.password,
|
|
62
|
+
name: value.name,
|
|
63
|
+
...invitationToken ? {
|
|
64
|
+
invitation_token: invitationToken
|
|
65
|
+
} : {}
|
|
66
|
+
});
|
|
67
|
+
const result = await signup.mutateAsync(body);
|
|
68
|
+
skipDraftPersistRef.current = true;
|
|
69
|
+
setAuthFormDraft(initialAuthFormDraft);
|
|
70
|
+
if (invitationToken && result.membership && invitationPending) {
|
|
71
|
+
try {
|
|
72
|
+
await refreshAuth();
|
|
73
|
+
} catch {
|
|
74
|
+
// Refresh failures don't block the success message — the next API
|
|
75
|
+
// call's 401 handling will re-route the user.
|
|
76
|
+
}
|
|
77
|
+
toast.success(`You joined ${invitationPending.workspace_name}.`);
|
|
78
|
+
await navigate({
|
|
79
|
+
to: '/workspaces/$wid',
|
|
80
|
+
params: {
|
|
81
|
+
wid: result.membership.workspace_id
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (invitationToken && result.accept_error) {
|
|
87
|
+
toast.error(result.accept_error.message);
|
|
88
|
+
await navigate({
|
|
89
|
+
to: '/invitations/accept',
|
|
90
|
+
search: {
|
|
91
|
+
token: invitationToken
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setSubmittedEmail(result.user.email);
|
|
97
|
+
setResendError(undefined);
|
|
98
|
+
restartLocalCooldown();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const mapped = signupErrorToFormError(error);
|
|
101
|
+
if (mapped.kind === 'field') {
|
|
102
|
+
form.setFieldMeta(mapped.field, (prev)=>({
|
|
103
|
+
...prev,
|
|
104
|
+
errorMap: {
|
|
105
|
+
...prev.errorMap,
|
|
106
|
+
onServer: mapped.message
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
} else {
|
|
110
|
+
setFormError(mapped.message);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
useEffect(()=>{
|
|
116
|
+
if (!submittedEmail || !nextResendAvailableAt) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const current = Date.now();
|
|
120
|
+
setNow(current);
|
|
121
|
+
if (nextResendAvailableAt <= current) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const handle = window.setInterval(()=>{
|
|
125
|
+
const tickNow = Date.now();
|
|
126
|
+
setNow(tickNow);
|
|
127
|
+
if (nextResendAvailableAt <= tickNow) {
|
|
128
|
+
window.clearInterval(handle);
|
|
129
|
+
}
|
|
130
|
+
}, 1000);
|
|
131
|
+
return ()=>window.clearInterval(handle);
|
|
132
|
+
}, [
|
|
133
|
+
nextResendAvailableAt,
|
|
134
|
+
submittedEmail
|
|
135
|
+
]);
|
|
136
|
+
function restartLocalCooldown() {
|
|
137
|
+
const current = Date.now();
|
|
138
|
+
setNow(current);
|
|
139
|
+
setNextResendAvailableAt(getLocalResendAvailableAt(current));
|
|
140
|
+
}
|
|
141
|
+
// When arriving from an invitation link, prefill the email and lock it.
|
|
142
|
+
useEffect(()=>{
|
|
143
|
+
if (invitationPending && form.state.values.email !== invitationPending.email) {
|
|
144
|
+
form.setFieldValue('email', invitationPending.email);
|
|
145
|
+
setAuthFormDraft((current)=>({
|
|
146
|
+
...current,
|
|
147
|
+
email: invitationPending.email
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
}, [
|
|
151
|
+
invitationPending,
|
|
152
|
+
form,
|
|
153
|
+
setAuthFormDraft
|
|
154
|
+
]);
|
|
155
|
+
// Sync form values back to the Jotai draft on unmount (only email + password
|
|
156
|
+
// — name is intentionally not persisted across navigation). Skipped after a
|
|
157
|
+
// successful signup because we just intentionally cleared the draft.
|
|
158
|
+
useEffect(()=>{
|
|
159
|
+
return ()=>{
|
|
160
|
+
if (skipDraftPersistRef.current) return;
|
|
161
|
+
const { email, password } = form.state.values;
|
|
162
|
+
if (email !== draftRef.current.email || password !== draftRef.current.password) {
|
|
163
|
+
setAuthFormDraft({
|
|
164
|
+
email,
|
|
165
|
+
password
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}, [
|
|
170
|
+
form,
|
|
171
|
+
setAuthFormDraft
|
|
172
|
+
]);
|
|
173
|
+
async function onResendVerificationEmail() {
|
|
174
|
+
if (!submittedEmail || isResendCoolingDown || resendEmailVerification.isPending) return;
|
|
175
|
+
setResendError(undefined);
|
|
176
|
+
try {
|
|
177
|
+
const result = await resendEmailVerification.mutateAsync({
|
|
178
|
+
email: submittedEmail
|
|
179
|
+
});
|
|
180
|
+
const nextAvailableAt = parseNextResendAvailableAt(result.next_resend_available_at);
|
|
181
|
+
setNow(Date.now());
|
|
182
|
+
if (nextAvailableAt !== undefined) {
|
|
183
|
+
setNextResendAvailableAt(nextAvailableAt);
|
|
184
|
+
} else {
|
|
185
|
+
restartLocalCooldown();
|
|
186
|
+
}
|
|
187
|
+
toast.success('If another verification email can be sent, it will arrive shortly.');
|
|
188
|
+
} catch (error) {
|
|
189
|
+
setResendError(authErrorMessage(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (submittedEmail) {
|
|
193
|
+
return /*#__PURE__*/ _jsxs(AuthShell, {
|
|
194
|
+
title: "Check your email",
|
|
195
|
+
description: `We sent a verification link to ${submittedEmail}.`,
|
|
196
|
+
children: [
|
|
197
|
+
resendError ? /*#__PURE__*/ _jsx(Callout, {
|
|
198
|
+
role: "alert",
|
|
199
|
+
type: "error",
|
|
200
|
+
children: resendError
|
|
201
|
+
}) : null,
|
|
202
|
+
/*#__PURE__*/ _jsx(Callout, {
|
|
203
|
+
role: "alert",
|
|
204
|
+
type: "success",
|
|
205
|
+
children: "Click the verification link to activate your account, then come back to log in."
|
|
206
|
+
}),
|
|
207
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
208
|
+
className: "flex flex-col gap-8",
|
|
209
|
+
children: [
|
|
210
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
211
|
+
"aria-disabled": isResendCoolingDown ? true : undefined,
|
|
212
|
+
className: "w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70",
|
|
213
|
+
variant: "secondary",
|
|
214
|
+
type: "button",
|
|
215
|
+
isLoading: resendEmailVerification.isPending,
|
|
216
|
+
onClick: onResendVerificationEmail,
|
|
217
|
+
children: resendEmailVerification.isPending ? 'Sending email...' : isResendCoolingDown ? `Resend in ${resendRemainingSeconds}s` : 'Resend verification email'
|
|
218
|
+
}),
|
|
219
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
220
|
+
className: "w-full",
|
|
221
|
+
variant: "transparent",
|
|
222
|
+
type: "button",
|
|
223
|
+
onClick: ()=>{
|
|
224
|
+
setSubmittedEmail(undefined);
|
|
225
|
+
setResendError(undefined);
|
|
226
|
+
},
|
|
227
|
+
children: "Use another email"
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
}),
|
|
231
|
+
/*#__PURE__*/ _jsxs(Text, {
|
|
232
|
+
size: "sm",
|
|
233
|
+
className: "text-center text-foreground-neutral-subtle",
|
|
234
|
+
children: [
|
|
235
|
+
"Already verified?",
|
|
236
|
+
' ',
|
|
237
|
+
/*#__PURE__*/ _jsx(ButtonLink, {
|
|
238
|
+
asChild: true,
|
|
239
|
+
variant: "interactive",
|
|
240
|
+
underline: true,
|
|
241
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
242
|
+
to: "/auth/login",
|
|
243
|
+
children: "Log in"
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
]
|
|
247
|
+
})
|
|
248
|
+
]
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const headerTitle = invitationPending ? `Join ${invitationPending.workspace_name}` : 'Create your Shipfox account';
|
|
252
|
+
const headerDescription = invitationPending ? `Create an account to accept your invitation.` : 'Start with your email and a password.';
|
|
253
|
+
const isInvitationEmailLocked = Boolean(invitationPending);
|
|
254
|
+
const invitationRedirect = invitationToken ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}` : undefined;
|
|
255
|
+
function persistDraft() {
|
|
256
|
+
const { email, password } = form.state.values;
|
|
257
|
+
setAuthFormDraft({
|
|
258
|
+
email,
|
|
259
|
+
password
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return /*#__PURE__*/ _jsxs(AuthShell, {
|
|
263
|
+
title: headerTitle,
|
|
264
|
+
description: headerDescription,
|
|
265
|
+
children: [
|
|
266
|
+
/*#__PURE__*/ _jsxs("form", {
|
|
267
|
+
className: "flex flex-col gap-18",
|
|
268
|
+
noValidate: true,
|
|
269
|
+
onSubmit: (event)=>{
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
event.stopPropagation();
|
|
272
|
+
void form.handleSubmit();
|
|
273
|
+
},
|
|
274
|
+
children: [
|
|
275
|
+
formError ? /*#__PURE__*/ _jsx(Callout, {
|
|
276
|
+
role: "alert",
|
|
277
|
+
type: "error",
|
|
278
|
+
children: formError
|
|
279
|
+
}) : null,
|
|
280
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
281
|
+
name: "name",
|
|
282
|
+
validators: {
|
|
283
|
+
onBlur: ({ value })=>displayNameFieldError(value, 'Name', signupBodySchema.shape.name),
|
|
284
|
+
onSubmit: ({ value })=>displayNameFieldError(value, 'Name', signupBodySchema.shape.name)
|
|
285
|
+
},
|
|
286
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
287
|
+
label: "Name",
|
|
288
|
+
id: "name",
|
|
289
|
+
error: fieldError(field),
|
|
290
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
291
|
+
autoComplete: "name",
|
|
292
|
+
name: "name",
|
|
293
|
+
type: "text",
|
|
294
|
+
value: field.state.value,
|
|
295
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
296
|
+
onBlur: field.handleBlur
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
}),
|
|
300
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
301
|
+
name: "email",
|
|
302
|
+
validators: {
|
|
303
|
+
onBlur: signupBodySchema.shape.email,
|
|
304
|
+
onSubmit: signupBodySchema.shape.email
|
|
305
|
+
},
|
|
306
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
307
|
+
label: "Email",
|
|
308
|
+
id: "email",
|
|
309
|
+
error: fieldError(field),
|
|
310
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
311
|
+
autoComplete: "email",
|
|
312
|
+
name: "email",
|
|
313
|
+
type: "email",
|
|
314
|
+
value: field.state.value,
|
|
315
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
316
|
+
onBlur: ()=>{
|
|
317
|
+
field.handleBlur();
|
|
318
|
+
persistDraft();
|
|
319
|
+
},
|
|
320
|
+
readOnly: isInvitationEmailLocked,
|
|
321
|
+
iconRight: isInvitationEmailLocked ? /*#__PURE__*/ _jsx(Icon, {
|
|
322
|
+
"aria-hidden": "true",
|
|
323
|
+
className: "size-16 text-foreground-neutral-disabled",
|
|
324
|
+
name: "lockLine"
|
|
325
|
+
}) : undefined
|
|
326
|
+
})
|
|
327
|
+
})
|
|
328
|
+
}),
|
|
329
|
+
/*#__PURE__*/ _jsx(form.Field, {
|
|
330
|
+
name: "password",
|
|
331
|
+
validators: {
|
|
332
|
+
onBlur: signupBodySchema.shape.password,
|
|
333
|
+
onSubmit: signupBodySchema.shape.password
|
|
334
|
+
},
|
|
335
|
+
children: (field)=>/*#__PURE__*/ _jsx(FormField, {
|
|
336
|
+
label: "Password",
|
|
337
|
+
id: "password",
|
|
338
|
+
error: fieldError(field),
|
|
339
|
+
children: /*#__PURE__*/ _jsx(FormFieldInput, {
|
|
340
|
+
autoComplete: "new-password",
|
|
341
|
+
name: "password",
|
|
342
|
+
type: "password",
|
|
343
|
+
value: field.state.value,
|
|
344
|
+
onChange: (event)=>field.handleChange(event.target.value),
|
|
345
|
+
onBlur: ()=>{
|
|
346
|
+
field.handleBlur();
|
|
347
|
+
persistDraft();
|
|
348
|
+
}
|
|
349
|
+
})
|
|
350
|
+
})
|
|
351
|
+
}),
|
|
352
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
353
|
+
className: "w-full",
|
|
354
|
+
isLoading: signup.isPending,
|
|
355
|
+
type: "submit",
|
|
356
|
+
children: signup.isPending ? 'Creating account...' : 'Create account'
|
|
357
|
+
})
|
|
358
|
+
]
|
|
359
|
+
}),
|
|
360
|
+
/*#__PURE__*/ _jsxs(Text, {
|
|
361
|
+
size: "sm",
|
|
362
|
+
className: "text-center text-foreground-neutral-subtle",
|
|
363
|
+
children: [
|
|
364
|
+
"Already have an account?",
|
|
365
|
+
' ',
|
|
366
|
+
/*#__PURE__*/ _jsx(ButtonLink, {
|
|
367
|
+
asChild: true,
|
|
368
|
+
variant: "interactive",
|
|
369
|
+
underline: true,
|
|
370
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
371
|
+
to: "/auth/login",
|
|
372
|
+
search: invitationRedirect ? {
|
|
373
|
+
redirect: invitationRedirect
|
|
374
|
+
} : undefined,
|
|
375
|
+
children: "Log in"
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
]
|
|
379
|
+
})
|
|
380
|
+
]
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
//# sourceMappingURL=signup-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/signup-page.tsx"],"sourcesContent":["import {signupBodySchema} from '@shipfox/api-auth-dto';\nimport {displayNameFieldError} from '@shipfox/client-ui';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useAtom} from 'jotai';\nimport {useEffect, useRef, useState} from 'react';\nimport {AuthShell} from '#/components/auth-shell.js';\nimport {useRefreshAuth} from '#hooks/api/refresh-auth.js';\nimport {useSignupAuth} from '#hooks/api/signup-auth.js';\nimport {useResendEmailVerificationAuth} from '#hooks/api/verify-email-auth.js';\nimport {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';\nimport {\n getLocalResendAvailableAt,\n getResendRemainingSeconds,\n parseNextResendAvailableAt,\n} from './email-verification-resend-model.js';\nimport {signupErrorToFormError} from './form-errors.js';\nimport {authErrorMessage} from './form-utils.js';\nimport {\n extractInvitationToken,\n pendingInvitation,\n useInvitationContext,\n} from './invitation-context.js';\n\nexport function SignupPage() {\n const signup = useSignupAuth();\n const resendEmailVerification = useResendEmailVerificationAuth();\n const refreshAuth = useRefreshAuth();\n const navigate = useNavigate();\n const search = useSearch({strict: false}) as {redirect?: unknown};\n const invitationToken = extractInvitationToken(search.redirect);\n const invitationPreview = useInvitationContext(invitationToken);\n const invitationPending = pendingInvitation(invitationPreview.data);\n const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);\n const [submittedEmail, setSubmittedEmail] = useState<string | undefined>();\n const [now, setNow] = useState(() => Date.now());\n const [nextResendAvailableAt, setNextResendAvailableAt] = useState<number | undefined>();\n const [formError, setFormError] = useState<string | undefined>();\n const [resendError, setResendError] = useState<string | undefined>();\n const draftRef = useRef(authFormDraft);\n draftRef.current = authFormDraft;\n // Set just before clearing the draft on success so the unmount cleanup\n // below does not repersist the just-submitted credentials.\n const skipDraftPersistRef = useRef(false);\n const resendRemainingSeconds = getResendRemainingSeconds({nextResendAvailableAt, now});\n const isResendCoolingDown = resendRemainingSeconds > 0;\n\n const form = useForm({\n defaultValues: {email: authFormDraft.email, password: authFormDraft.password, name: ''},\n onSubmit: async ({value}) => {\n setFormError(undefined);\n try {\n const body = signupBodySchema.parse({\n email: value.email,\n password: value.password,\n name: value.name,\n ...(invitationToken ? {invitation_token: invitationToken} : {}),\n });\n const result = await signup.mutateAsync(body);\n skipDraftPersistRef.current = true;\n setAuthFormDraft(initialAuthFormDraft);\n\n if (invitationToken && result.membership && invitationPending) {\n try {\n await refreshAuth();\n } catch {\n // Refresh failures don't block the success message — the next API\n // call's 401 handling will re-route the user.\n }\n toast.success(`You joined ${invitationPending.workspace_name}.`);\n await navigate({\n to: '/workspaces/$wid',\n params: {wid: result.membership.workspace_id},\n });\n return;\n }\n\n if (invitationToken && result.accept_error) {\n toast.error(result.accept_error.message);\n await navigate({\n to: '/invitations/accept',\n search: {token: invitationToken},\n });\n return;\n }\n\n setSubmittedEmail(result.user.email);\n setResendError(undefined);\n restartLocalCooldown();\n } catch (error) {\n const mapped = signupErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (prev) => ({\n ...prev,\n errorMap: {...prev.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n },\n });\n\n useEffect(() => {\n if (!submittedEmail || !nextResendAvailableAt) {\n return;\n }\n\n const current = Date.now();\n setNow(current);\n if (nextResendAvailableAt <= current) {\n return;\n }\n\n const handle = window.setInterval(() => {\n const tickNow = Date.now();\n setNow(tickNow);\n if (nextResendAvailableAt <= tickNow) {\n window.clearInterval(handle);\n }\n }, 1000);\n return () => window.clearInterval(handle);\n }, [nextResendAvailableAt, submittedEmail]);\n\n function restartLocalCooldown() {\n const current = Date.now();\n setNow(current);\n setNextResendAvailableAt(getLocalResendAvailableAt(current));\n }\n\n // When arriving from an invitation link, prefill the email and lock it.\n useEffect(() => {\n if (invitationPending && form.state.values.email !== invitationPending.email) {\n form.setFieldValue('email', invitationPending.email);\n setAuthFormDraft((current) => ({...current, email: invitationPending.email}));\n }\n }, [invitationPending, form, setAuthFormDraft]);\n\n // Sync form values back to the Jotai draft on unmount (only email + password\n // — name is intentionally not persisted across navigation). Skipped after a\n // successful signup because we just intentionally cleared the draft.\n useEffect(() => {\n return () => {\n if (skipDraftPersistRef.current) return;\n const {email, password} = form.state.values;\n if (email !== draftRef.current.email || password !== draftRef.current.password) {\n setAuthFormDraft({email, password});\n }\n };\n }, [form, setAuthFormDraft]);\n\n async function onResendVerificationEmail() {\n if (!submittedEmail || isResendCoolingDown || resendEmailVerification.isPending) return;\n\n setResendError(undefined);\n try {\n const result = await resendEmailVerification.mutateAsync({email: submittedEmail});\n const nextAvailableAt = parseNextResendAvailableAt(result.next_resend_available_at);\n setNow(Date.now());\n if (nextAvailableAt !== undefined) {\n setNextResendAvailableAt(nextAvailableAt);\n } else {\n restartLocalCooldown();\n }\n toast.success('If another verification email can be sent, it will arrive shortly.');\n } catch (error) {\n setResendError(authErrorMessage(error));\n }\n }\n\n if (submittedEmail) {\n return (\n <AuthShell\n title=\"Check your email\"\n description={`We sent a verification link to ${submittedEmail}.`}\n >\n {resendError ? (\n <Callout role=\"alert\" type=\"error\">\n {resendError}\n </Callout>\n ) : null}\n <Callout role=\"alert\" type=\"success\">\n Click the verification link to activate your account, then come back to log in.\n </Callout>\n <div className=\"flex flex-col gap-8\">\n <Button\n aria-disabled={isResendCoolingDown ? true : undefined}\n className=\"w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70\"\n variant=\"secondary\"\n type=\"button\"\n isLoading={resendEmailVerification.isPending}\n onClick={onResendVerificationEmail}\n >\n {resendEmailVerification.isPending\n ? 'Sending email...'\n : isResendCoolingDown\n ? `Resend in ${resendRemainingSeconds}s`\n : 'Resend verification email'}\n </Button>\n <Button\n className=\"w-full\"\n variant=\"transparent\"\n type=\"button\"\n onClick={() => {\n setSubmittedEmail(undefined);\n setResendError(undefined);\n }}\n >\n Use another email\n </Button>\n </div>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n Already verified?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link to=\"/auth/login\">Log in</Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n }\n\n const headerTitle = invitationPending\n ? `Join ${invitationPending.workspace_name}`\n : 'Create your Shipfox account';\n const headerDescription = invitationPending\n ? `Create an account to accept your invitation.`\n : 'Start with your email and a password.';\n const isInvitationEmailLocked = Boolean(invitationPending);\n const invitationRedirect = invitationToken\n ? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`\n : undefined;\n\n function persistDraft() {\n const {email, password} = form.state.values;\n setAuthFormDraft({email, password});\n }\n\n return (\n <AuthShell title={headerTitle} description={headerDescription}>\n <form\n className=\"flex flex-col gap-18\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n <form.Field\n name=\"name\"\n validators={{\n onBlur: ({value}) => displayNameFieldError(value, 'Name', signupBodySchema.shape.name),\n onSubmit: ({value}) =>\n displayNameFieldError(value, 'Name', signupBodySchema.shape.name),\n }}\n >\n {(field) => (\n <FormField label=\"Name\" id=\"name\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"name\"\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={field.handleBlur}\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"email\"\n validators={{\n onBlur: signupBodySchema.shape.email,\n onSubmit: signupBodySchema.shape.email,\n }}\n >\n {(field) => (\n <FormField label=\"Email\" id=\"email\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"email\"\n name=\"email\"\n type=\"email\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n readOnly={isInvitationEmailLocked}\n iconRight={\n isInvitationEmailLocked ? (\n <Icon\n aria-hidden=\"true\"\n className=\"size-16 text-foreground-neutral-disabled\"\n name=\"lockLine\"\n />\n ) : undefined\n }\n />\n </FormField>\n )}\n </form.Field>\n <form.Field\n name=\"password\"\n validators={{\n onBlur: signupBodySchema.shape.password,\n onSubmit: signupBodySchema.shape.password,\n }}\n >\n {(field) => (\n <FormField label=\"Password\" id=\"password\" error={fieldError(field)}>\n <FormFieldInput\n autoComplete=\"new-password\"\n name=\"password\"\n type=\"password\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={() => {\n field.handleBlur();\n persistDraft();\n }}\n />\n </FormField>\n )}\n </form.Field>\n <Button className=\"w-full\" isLoading={signup.isPending} type=\"submit\">\n {signup.isPending ? 'Creating account...' : 'Create account'}\n </Button>\n </form>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-subtle\">\n Already have an account?{' '}\n <ButtonLink asChild variant=\"interactive\" underline>\n <Link\n to=\"/auth/login\"\n search={invitationRedirect ? {redirect: invitationRedirect} : undefined}\n >\n Log in\n </Link>\n </ButtonLink>\n </Text>\n </AuthShell>\n );\n}\n"],"names":["signupBodySchema","displayNameFieldError","Button","ButtonLink","Callout","FormField","FormFieldInput","fieldError","Icon","toast","Text","useForm","Link","useNavigate","useSearch","useAtom","useEffect","useRef","useState","AuthShell","useRefreshAuth","useSignupAuth","useResendEmailVerificationAuth","authFormDraftAtom","initialAuthFormDraft","getLocalResendAvailableAt","getResendRemainingSeconds","parseNextResendAvailableAt","signupErrorToFormError","authErrorMessage","extractInvitationToken","pendingInvitation","useInvitationContext","SignupPage","signup","resendEmailVerification","refreshAuth","navigate","search","strict","invitationToken","redirect","invitationPreview","invitationPending","data","authFormDraft","setAuthFormDraft","submittedEmail","setSubmittedEmail","now","setNow","Date","nextResendAvailableAt","setNextResendAvailableAt","formError","setFormError","resendError","setResendError","draftRef","current","skipDraftPersistRef","resendRemainingSeconds","isResendCoolingDown","form","defaultValues","email","password","name","onSubmit","value","undefined","body","parse","invitation_token","result","mutateAsync","membership","success","workspace_name","to","params","wid","workspace_id","accept_error","error","message","token","user","restartLocalCooldown","mapped","kind","setFieldMeta","field","prev","errorMap","onServer","handle","window","setInterval","tickNow","clearInterval","state","values","setFieldValue","onResendVerificationEmail","isPending","nextAvailableAt","next_resend_available_at","title","description","role","type","div","className","aria-disabled","variant","isLoading","onClick","size","asChild","underline","headerTitle","headerDescription","isInvitationEmailLocked","Boolean","invitationRedirect","encodeURIComponent","persistDraft","noValidate","event","preventDefault","stopPropagation","handleSubmit","Field","validators","onBlur","shape","label","id","autoComplete","onChange","handleChange","target","handleBlur","readOnly","iconRight","aria-hidden"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-email-page.d.ts","sourceRoot":"","sources":["../../src/pages/verify-email-page.tsx"],"names":[],"mappings":"AAUA,wBAAgB,eAAe,SAqC9B"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { toast } from '@shipfox/react-ui/toast';
|
|
2
|
+
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useRefreshAuth } from '#hooks/api/refresh-auth.js';
|
|
5
|
+
import { useVerifyEmailAuth } from '#hooks/api/verify-email-auth.js';
|
|
6
|
+
import { authErrorMessage } from './form-utils.js';
|
|
7
|
+
const verificationRequests = new Map();
|
|
8
|
+
const toastedVerificationTokens = new Set();
|
|
9
|
+
export function VerifyEmailPage() {
|
|
10
|
+
const verifyEmail = useVerifyEmailAuth();
|
|
11
|
+
const refreshAuth = useRefreshAuth();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const search = useSearch({
|
|
14
|
+
strict: false
|
|
15
|
+
});
|
|
16
|
+
const token = typeof search.token === 'string' ? search.token : undefined;
|
|
17
|
+
useEffect(()=>{
|
|
18
|
+
if (!token) {
|
|
19
|
+
toast.error('This verification link is missing a token.');
|
|
20
|
+
navigate({
|
|
21
|
+
to: '/',
|
|
22
|
+
replace: true
|
|
23
|
+
});
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let request = verificationRequests.get(token);
|
|
27
|
+
if (!request) {
|
|
28
|
+
request = verifyEmail.mutateAsync({
|
|
29
|
+
token
|
|
30
|
+
}).then(async ()=>{
|
|
31
|
+
await refreshAuth();
|
|
32
|
+
});
|
|
33
|
+
verificationRequests.set(token, request);
|
|
34
|
+
}
|
|
35
|
+
request.then(()=>{
|
|
36
|
+
if (toastedVerificationTokens.has(token)) return;
|
|
37
|
+
toastedVerificationTokens.add(token);
|
|
38
|
+
toast.success('Your email is verified. You are now logged in.');
|
|
39
|
+
navigate({
|
|
40
|
+
to: '/',
|
|
41
|
+
replace: true
|
|
42
|
+
});
|
|
43
|
+
}).catch((error)=>{
|
|
44
|
+
if (toastedVerificationTokens.has(token)) return;
|
|
45
|
+
toastedVerificationTokens.add(token);
|
|
46
|
+
toast.error(authErrorMessage(error));
|
|
47
|
+
});
|
|
48
|
+
}, [
|
|
49
|
+
navigate,
|
|
50
|
+
refreshAuth,
|
|
51
|
+
token,
|
|
52
|
+
verifyEmail.mutateAsync
|
|
53
|
+
]);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=verify-email-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/pages/verify-email-page.tsx"],"sourcesContent":["import {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect} from 'react';\nimport {useRefreshAuth} from '#hooks/api/refresh-auth.js';\nimport {useVerifyEmailAuth} from '#hooks/api/verify-email-auth.js';\nimport {authErrorMessage} from './form-utils.js';\n\nconst verificationRequests = new Map<string, Promise<void>>();\nconst toastedVerificationTokens = new Set<string>();\n\nexport function VerifyEmailPage() {\n const verifyEmail = useVerifyEmailAuth();\n const refreshAuth = useRefreshAuth();\n const navigate = useNavigate();\n const search = useSearch({strict: false});\n const token = typeof search.token === 'string' ? search.token : undefined;\n\n useEffect(() => {\n if (!token) {\n toast.error('This verification link is missing a token.');\n navigate({to: '/', replace: true});\n return;\n }\n\n let request = verificationRequests.get(token);\n if (!request) {\n request = verifyEmail.mutateAsync({token}).then(async () => {\n await refreshAuth();\n });\n verificationRequests.set(token, request);\n }\n\n request\n .then(() => {\n if (toastedVerificationTokens.has(token)) return;\n toastedVerificationTokens.add(token);\n toast.success('Your email is verified. You are now logged in.');\n navigate({to: '/', replace: true});\n })\n .catch((error) => {\n if (toastedVerificationTokens.has(token)) return;\n toastedVerificationTokens.add(token);\n toast.error(authErrorMessage(error));\n });\n }, [navigate, refreshAuth, token, verifyEmail.mutateAsync]);\n\n return null;\n}\n"],"names":["toast","useNavigate","useSearch","useEffect","useRefreshAuth","useVerifyEmailAuth","authErrorMessage","verificationRequests","Map","toastedVerificationTokens","Set","VerifyEmailPage","verifyEmail","refreshAuth","navigate","search","strict","token","undefined","error","to","replace","request","get","mutateAsync","then","set","has","add","success","catch"],"mappings":"AAAA,SAAQA,KAAK,QAAO,0BAA0B;AAC9C,SAAQC,WAAW,EAAEC,SAAS,QAAO,yBAAyB;AAC9D,SAAQC,SAAS,QAAO,QAAQ;AAChC,SAAQC,cAAc,QAAO,6BAA6B;AAC1D,SAAQC,kBAAkB,QAAO,kCAAkC;AACnE,SAAQC,gBAAgB,QAAO,kBAAkB;AAEjD,MAAMC,uBAAuB,IAAIC;AACjC,MAAMC,4BAA4B,IAAIC;AAEtC,OAAO,SAASC;IACd,MAAMC,cAAcP;IACpB,MAAMQ,cAAcT;IACpB,MAAMU,WAAWb;IACjB,MAAMc,SAASb,UAAU;QAACc,QAAQ;IAAK;IACvC,MAAMC,QAAQ,OAAOF,OAAOE,KAAK,KAAK,WAAWF,OAAOE,KAAK,GAAGC;IAEhEf,UAAU;QACR,IAAI,CAACc,OAAO;YACVjB,MAAMmB,KAAK,CAAC;YACZL,SAAS;gBAACM,IAAI;gBAAKC,SAAS;YAAI;YAChC;QACF;QAEA,IAAIC,UAAUf,qBAAqBgB,GAAG,CAACN;QACvC,IAAI,CAACK,SAAS;YACZA,UAAUV,YAAYY,WAAW,CAAC;gBAACP;YAAK,GAAGQ,IAAI,CAAC;gBAC9C,MAAMZ;YACR;YACAN,qBAAqBmB,GAAG,CAACT,OAAOK;QAClC;QAEAA,QACGG,IAAI,CAAC;YACJ,IAAIhB,0BAA0BkB,GAAG,CAACV,QAAQ;YAC1CR,0BAA0BmB,GAAG,CAACX;YAC9BjB,MAAM6B,OAAO,CAAC;YACdf,SAAS;gBAACM,IAAI;gBAAKC,SAAS;YAAI;QAClC,GACCS,KAAK,CAAC,CAACX;YACN,IAAIV,0BAA0BkB,GAAG,CAACV,QAAQ;YAC1CR,0BAA0BmB,GAAG,CAACX;YAC9BjB,MAAMmB,KAAK,CAACb,iBAAiBa;QAC/B;IACJ,GAAG;QAACL;QAAUD;QAAaI;QAAOL,YAAYY,WAAW;KAAC;IAE1D,OAAO;AACT"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-onboarding-page.d.ts","sourceRoot":"","sources":["../../src/pages/workspace-onboarding-page.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,uBAAuB,gCA2JtC"}
|