@shipfox/client-auth 3.0.1 → 4.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/components/auth-shell.d.ts +6 -4
- package/dist/components/auth-shell.d.ts.map +1 -1
- package/dist/components/auth-shell.js +6 -1
- package/dist/components/auth-shell.js.map +1 -1
- package/dist/components/email-code-verification.d.ts +20 -0
- package/dist/components/email-code-verification.d.ts.map +1 -0
- package/dist/components/email-code-verification.js +136 -0
- package/dist/components/email-code-verification.js.map +1 -0
- package/dist/components/redirect-context.d.ts +11 -0
- package/dist/components/redirect-context.d.ts.map +1 -0
- package/dist/components/redirect-context.js +22 -0
- package/dist/components/redirect-context.js.map +1 -0
- package/dist/components/redirect-target.d.ts.map +1 -1
- package/dist/components/redirect-target.js +13 -12
- package/dist/components/redirect-target.js.map +1 -1
- package/dist/continuation.d.ts +4 -0
- package/dist/continuation.d.ts.map +1 -0
- package/dist/continuation.js +5 -0
- package/dist/continuation.js.map +1 -0
- package/dist/hooks/api/login-auth.d.ts.map +1 -1
- package/dist/hooks/api/login-auth.js +4 -28
- package/dist/hooks/api/login-auth.js.map +1 -1
- package/dist/hooks/api/logout-auth.d.ts.map +1 -1
- package/dist/hooks/api/logout-auth.js +4 -14
- package/dist/hooks/api/logout-auth.js.map +1 -1
- package/dist/hooks/api/password-reset-auth.d.ts.map +1 -1
- package/dist/hooks/api/password-reset-auth.js +4 -23
- package/dist/hooks/api/password-reset-auth.js.map +1 -1
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
- package/dist/hooks/api/verify-email-auth.js +4 -23
- package/dist/hooks/api/verify-email-auth.js.map +1 -1
- package/dist/pages/invitation-context.d.ts +0 -5
- package/dist/pages/invitation-context.d.ts.map +1 -1
- package/dist/pages/invitation-context.js +3 -19
- package/dist/pages/invitation-context.js.map +1 -1
- package/dist/pages/signup-page.d.ts.map +1 -1
- package/dist/pages/signup-page.js +50 -144
- package/dist/pages/signup-page.js.map +1 -1
- package/dist/state/auth.d.ts +1 -1
- package/dist/state/auth.d.ts.map +1 -1
- package/dist/state/auth.js +1 -1
- package/dist/state/auth.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +9 -39
- package/src/components/auth-provider.test.tsx +173 -3
- package/src/components/auth-shell.tsx +20 -5
- package/src/components/email-code-verification.test.tsx +159 -0
- package/src/components/email-code-verification.tsx +177 -0
- package/src/components/redirect-context.test.ts +51 -0
- package/src/components/redirect-context.ts +26 -0
- package/src/components/redirect-target.test.ts +8 -0
- package/src/components/redirect-target.ts +13 -12
- package/src/continuation.test.ts +15 -0
- package/src/continuation.ts +6 -0
- package/src/hooks/api/login-auth.ts +4 -28
- package/src/hooks/api/logout-auth.ts +4 -10
- package/src/hooks/api/password-reset-auth.ts +4 -23
- package/src/hooks/api/verify-email-auth.ts +4 -23
- package/src/pages/invitation-context.ts +2 -20
- package/src/pages/signup-page.tsx +33 -129
- package/src/state/auth.ts +1 -0
- package/src/state/last-workspace.test.ts +16 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/pages/email-verification-resend-model.d.ts +0 -6
- package/dist/pages/email-verification-resend-model.d.ts.map +0 -1
- package/dist/pages/email-verification-resend-model.js +0 -11
- package/dist/pages/email-verification-resend-model.js.map +0 -1
- package/src/pages/email-verification-resend-model.test.ts +0 -42
- package/src/pages/email-verification-resend-model.ts +0 -15
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {signupBodySchema
|
|
1
|
+
import {signupBodySchema} from '@shipfox/api-auth-dto';
|
|
2
2
|
import {displayNameFieldError} from '@shipfox/client-ui';
|
|
3
3
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
4
4
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -11,14 +11,11 @@ import {Link, useNavigate, useSearch} from '@tanstack/react-router';
|
|
|
11
11
|
import {useAtom} from 'jotai';
|
|
12
12
|
import {useEffect, useRef, useState} from 'react';
|
|
13
13
|
import {AuthShell} from '#/components/auth-shell.js';
|
|
14
|
+
import {EmailCodeVerification} from '#/components/email-code-verification.js';
|
|
14
15
|
import {useRefreshAuth} from '#hooks/api/refresh-auth.js';
|
|
15
16
|
import {useSignupAuth} from '#hooks/api/signup-auth.js';
|
|
16
17
|
import {useResendEmailVerificationAuth, useVerifyEmailAuth} from '#hooks/api/verify-email-auth.js';
|
|
17
18
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
18
|
-
import {
|
|
19
|
-
getResendRemainingSeconds,
|
|
20
|
-
parseNextResendAvailableAt,
|
|
21
|
-
} from './email-verification-resend-model.js';
|
|
22
19
|
import {signupErrorToFormError} from './form-errors.js';
|
|
23
20
|
import {authErrorMessage} from './form-utils.js';
|
|
24
21
|
import {
|
|
@@ -39,8 +36,7 @@ export function SignupPage() {
|
|
|
39
36
|
const invitationPending = pendingInvitation(invitationPreview.data);
|
|
40
37
|
const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
41
38
|
const [emailChallenge, setEmailChallenge] = useState<{email: string; id: string} | undefined>();
|
|
42
|
-
const [
|
|
43
|
-
const [nextResendAvailableAt, setNextResendAvailableAt] = useState<number | undefined>();
|
|
39
|
+
const [nextResendAvailableAt, setNextResendAvailableAt] = useState<string | undefined>();
|
|
44
40
|
const [formError, setFormError] = useState<string | undefined>();
|
|
45
41
|
const [resendError, setResendError] = useState<string | undefined>();
|
|
46
42
|
const draftRef = useRef(authFormDraft);
|
|
@@ -48,8 +44,6 @@ export function SignupPage() {
|
|
|
48
44
|
// Set just before clearing the draft on success so the unmount cleanup
|
|
49
45
|
// below does not repersist the just-submitted credentials.
|
|
50
46
|
const skipDraftPersistRef = useRef(false);
|
|
51
|
-
const resendRemainingSeconds = getResendRemainingSeconds({nextResendAvailableAt, now});
|
|
52
|
-
const isResendCoolingDown = resendRemainingSeconds > 0;
|
|
53
47
|
|
|
54
48
|
const form = useForm({
|
|
55
49
|
defaultValues: {email: authFormDraft.email, password: authFormDraft.password, name: ''},
|
|
@@ -94,12 +88,8 @@ export function SignupPage() {
|
|
|
94
88
|
throw new Error('Signup did not return an email verification challenge');
|
|
95
89
|
}
|
|
96
90
|
setEmailChallenge({email: result.user.email, id: result.email_challenge.id});
|
|
97
|
-
verificationForm.reset();
|
|
98
91
|
setResendError(undefined);
|
|
99
|
-
|
|
100
|
-
setNextResendAvailableAt(
|
|
101
|
-
parseNextResendAvailableAt(result.email_challenge.next_resend_available_at),
|
|
102
|
-
);
|
|
92
|
+
setNextResendAvailableAt(result.email_challenge.next_resend_available_at);
|
|
103
93
|
} catch (error) {
|
|
104
94
|
const mapped = signupErrorToFormError(error);
|
|
105
95
|
if (mapped.kind === 'field') {
|
|
@@ -114,48 +104,6 @@ export function SignupPage() {
|
|
|
114
104
|
},
|
|
115
105
|
});
|
|
116
106
|
|
|
117
|
-
const verificationForm = useForm({
|
|
118
|
-
defaultValues: {code: ''},
|
|
119
|
-
onSubmit: async ({value}) => {
|
|
120
|
-
if (!emailChallenge || verifyEmail.isPending) return;
|
|
121
|
-
|
|
122
|
-
setResendError(undefined);
|
|
123
|
-
try {
|
|
124
|
-
await verifyEmail.mutateAsync({
|
|
125
|
-
email: emailChallenge.email,
|
|
126
|
-
challenge_id: emailChallenge.id,
|
|
127
|
-
code: value.code,
|
|
128
|
-
});
|
|
129
|
-
await refreshAuth();
|
|
130
|
-
toast.success('Your email is verified. You are now logged in.');
|
|
131
|
-
await navigate({to: '/', replace: true});
|
|
132
|
-
} catch (error) {
|
|
133
|
-
setResendError(authErrorMessage(error));
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
useEffect(() => {
|
|
139
|
-
if (!emailChallenge || !nextResendAvailableAt) {
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const current = Date.now();
|
|
144
|
-
setNow(current);
|
|
145
|
-
if (nextResendAvailableAt <= current) {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const handle = window.setInterval(() => {
|
|
150
|
-
const tickNow = Date.now();
|
|
151
|
-
setNow(tickNow);
|
|
152
|
-
if (nextResendAvailableAt <= tickNow) {
|
|
153
|
-
window.clearInterval(handle);
|
|
154
|
-
}
|
|
155
|
-
}, 1000);
|
|
156
|
-
return () => window.clearInterval(handle);
|
|
157
|
-
}, [emailChallenge, nextResendAvailableAt]);
|
|
158
|
-
|
|
159
107
|
// When arriving from an invitation link, prefill the email and lock it.
|
|
160
108
|
useEffect(() => {
|
|
161
109
|
if (invitationPending && form.state.values.email !== invitationPending.email) {
|
|
@@ -178,7 +126,7 @@ export function SignupPage() {
|
|
|
178
126
|
}, [form, setAuthFormDraft]);
|
|
179
127
|
|
|
180
128
|
async function onResendVerificationEmail() {
|
|
181
|
-
if (!emailChallenge ||
|
|
129
|
+
if (!emailChallenge || resendEmailVerification.isPending) return;
|
|
182
130
|
|
|
183
131
|
setResendError(undefined);
|
|
184
132
|
try {
|
|
@@ -186,11 +134,7 @@ export function SignupPage() {
|
|
|
186
134
|
email: emailChallenge.email,
|
|
187
135
|
challenge_id: emailChallenge.id,
|
|
188
136
|
});
|
|
189
|
-
|
|
190
|
-
setNow(Date.now());
|
|
191
|
-
if (nextAvailableAt !== undefined) {
|
|
192
|
-
setNextResendAvailableAt(nextAvailableAt);
|
|
193
|
-
}
|
|
137
|
+
setNextResendAvailableAt(result.next_resend_available_at);
|
|
194
138
|
toast.success('If another verification email can be sent, it will arrive shortly.');
|
|
195
139
|
} catch (error) {
|
|
196
140
|
setResendError(authErrorMessage(error));
|
|
@@ -199,80 +143,40 @@ export function SignupPage() {
|
|
|
199
143
|
|
|
200
144
|
if (emailChallenge) {
|
|
201
145
|
return (
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
146
|
+
<EmailCodeVerification
|
|
147
|
+
destination={emailChallenge.email}
|
|
148
|
+
nextResendAvailableAt={nextResendAvailableAt}
|
|
149
|
+
isResending={resendEmailVerification.isPending}
|
|
150
|
+
isVerifying={verifyEmail.isPending}
|
|
151
|
+
error={resendError}
|
|
152
|
+
onVerify={async (code) => {
|
|
153
|
+
setResendError(undefined);
|
|
154
|
+
try {
|
|
155
|
+
await verifyEmail.mutateAsync({
|
|
156
|
+
email: emailChallenge.email,
|
|
157
|
+
challenge_id: emailChallenge.id,
|
|
158
|
+
code,
|
|
159
|
+
});
|
|
160
|
+
await refreshAuth();
|
|
161
|
+
toast.success('Your email is verified. You are now logged in.');
|
|
162
|
+
await navigate({to: '/', replace: true});
|
|
163
|
+
} catch (error) {
|
|
164
|
+
setResendError(authErrorMessage(error));
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
onResend={onResendVerificationEmail}
|
|
168
|
+
onUseAnotherEmail={() => {
|
|
169
|
+
setEmailChallenge(undefined);
|
|
170
|
+
setResendError(undefined);
|
|
171
|
+
}}
|
|
205
172
|
>
|
|
206
|
-
{resendError ? (
|
|
207
|
-
<Callout role="alert" type="error">
|
|
208
|
-
{resendError}
|
|
209
|
-
</Callout>
|
|
210
|
-
) : null}
|
|
211
|
-
<form
|
|
212
|
-
className="flex flex-col gap-8"
|
|
213
|
-
noValidate
|
|
214
|
-
onSubmit={(event) => {
|
|
215
|
-
event.preventDefault();
|
|
216
|
-
void verificationForm.handleSubmit();
|
|
217
|
-
}}
|
|
218
|
-
>
|
|
219
|
-
<verificationForm.Field
|
|
220
|
-
name="code"
|
|
221
|
-
validators={{
|
|
222
|
-
onBlur: verifyEmailConfirmBodySchema.shape.code,
|
|
223
|
-
onSubmit: verifyEmailConfirmBodySchema.shape.code,
|
|
224
|
-
}}
|
|
225
|
-
>
|
|
226
|
-
{(field) => (
|
|
227
|
-
<FormField label="Verification code" id="verification-code" error={fieldError(field)}>
|
|
228
|
-
<FormFieldInput
|
|
229
|
-
autoComplete="one-time-code"
|
|
230
|
-
inputMode="numeric"
|
|
231
|
-
maxLength={8}
|
|
232
|
-
pattern="[0-9]{8}"
|
|
233
|
-
value={field.state.value}
|
|
234
|
-
onChange={(event) => field.handleChange(event.target.value.replace(/\D/gu, ''))}
|
|
235
|
-
onBlur={field.handleBlur}
|
|
236
|
-
/>
|
|
237
|
-
</FormField>
|
|
238
|
-
)}
|
|
239
|
-
</verificationForm.Field>
|
|
240
|
-
<Button className="w-full" type="submit" isLoading={verifyEmail.isPending}>
|
|
241
|
-
Verify email
|
|
242
|
-
</Button>
|
|
243
|
-
<Button
|
|
244
|
-
aria-disabled={isResendCoolingDown ? true : undefined}
|
|
245
|
-
className="w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70"
|
|
246
|
-
variant="secondary"
|
|
247
|
-
type="button"
|
|
248
|
-
isLoading={resendEmailVerification.isPending}
|
|
249
|
-
onClick={onResendVerificationEmail}
|
|
250
|
-
>
|
|
251
|
-
{resendEmailVerification.isPending
|
|
252
|
-
? 'Sending email...'
|
|
253
|
-
: isResendCoolingDown
|
|
254
|
-
? `Resend in ${resendRemainingSeconds}s`
|
|
255
|
-
: 'Resend verification email'}
|
|
256
|
-
</Button>
|
|
257
|
-
</form>
|
|
258
|
-
<Button
|
|
259
|
-
className="w-full"
|
|
260
|
-
variant="transparent"
|
|
261
|
-
type="button"
|
|
262
|
-
onClick={() => {
|
|
263
|
-
setEmailChallenge(undefined);
|
|
264
|
-
setResendError(undefined);
|
|
265
|
-
}}
|
|
266
|
-
>
|
|
267
|
-
Use another email
|
|
268
|
-
</Button>
|
|
269
173
|
<Text size="sm" className="text-center text-foreground-neutral-subtle">
|
|
270
174
|
Already verified?{' '}
|
|
271
175
|
<ButtonLink asChild variant="interactive" underline>
|
|
272
176
|
<Link to="/auth/login">Log in</Link>
|
|
273
177
|
</ButtonLink>
|
|
274
178
|
</Text>
|
|
275
|
-
</
|
|
179
|
+
</EmailCodeVerification>
|
|
276
180
|
);
|
|
277
181
|
}
|
|
278
182
|
|
package/src/state/auth.ts
CHANGED
|
@@ -45,6 +45,22 @@ describe('lastWorkspaceIdAtom', () => {
|
|
|
45
45
|
unsubscribe();
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
test('syncs the atom when another tab updates last workspace storage', () => {
|
|
49
|
+
const store = createStore();
|
|
50
|
+
const unsubscribe = store.sub(lastWorkspaceIdAtom, () => undefined);
|
|
51
|
+
window.localStorage.setItem('shipfox.lastWorkspaceId', JSON.stringify('workspace-2'));
|
|
52
|
+
|
|
53
|
+
window.dispatchEvent(
|
|
54
|
+
new StorageEvent('storage', {
|
|
55
|
+
key: 'shipfox.lastWorkspaceId',
|
|
56
|
+
storageArea: window.localStorage,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(store.get(lastWorkspaceIdAtom)).toBe('workspace-2');
|
|
61
|
+
unsubscribe();
|
|
62
|
+
});
|
|
63
|
+
|
|
48
64
|
test('rememberLastWorkspaceId persists a root redirect target', () => {
|
|
49
65
|
rememberLastWorkspaceId('workspace-1');
|
|
50
66
|
|