@shipfox/client-auth 0.2.0 → 2.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 +39 -0
- package/dist/feature.d.ts +0 -4
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +0 -5
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/signup-auth.d.ts +4 -0
- package/dist/hooks/api/signup-auth.d.ts.map +1 -1
- package/dist/hooks/api/verify-email-auth.d.ts +4 -1
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/email-verification-resend-model.d.ts +0 -1
- package/dist/pages/email-verification-resend-model.d.ts.map +1 -1
- package/dist/pages/email-verification-resend-model.js +0 -4
- package/dist/pages/email-verification-resend-model.js.map +1 -1
- package/dist/pages/signup-page.d.ts.map +1 -1
- package/dist/pages/signup-page.js +90 -36
- package/dist/pages/signup-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +19 -42
- package/src/feature.ts +0 -1
- package/src/index.ts +0 -1
- package/src/pages/email-verification-resend-model.test.ts +0 -10
- package/src/pages/email-verification-resend-model.ts +0 -6
- package/src/pages/signup-page.test.tsx +60 -6
- package/src/pages/signup-page.tsx +87 -36
- package/test/pages.tsx +0 -7
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/pages/verify-email-page.d.ts +0 -2
- package/dist/pages/verify-email-page.d.ts.map +0 -1
- package/dist/pages/verify-email-page.js +0 -57
- package/dist/pages/verify-email-page.js.map +0 -1
- package/dist/routes/verify-email.d.ts +0 -5
- package/dist/routes/verify-email.d.ts.map +0 -1
- package/dist/routes/verify-email.js +0 -11
- package/dist/routes/verify-email.js.map +0 -1
- package/src/pages/verify-email-page.test.tsx +0 -82
- package/src/pages/verify-email-page.tsx +0 -48
- package/src/routes/verify-email.tsx +0 -11
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "2.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -11,55 +11,32 @@
|
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
13
|
"imports": {
|
|
14
|
-
"#*":
|
|
15
|
-
"types": "./src/*",
|
|
16
|
-
"workspace-source": "./src/*",
|
|
17
|
-
"development": "./src/*",
|
|
18
|
-
"default": "./dist/*"
|
|
19
|
-
},
|
|
14
|
+
"#*": "./dist/*",
|
|
20
15
|
"#test/*": "./test/*"
|
|
21
16
|
},
|
|
22
17
|
"exports": {
|
|
23
18
|
".": {
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
"default": "./src/index.ts"
|
|
27
|
-
},
|
|
28
|
-
"default": {
|
|
29
|
-
"types": "./dist/index.d.ts",
|
|
30
|
-
"default": "./dist/index.js"
|
|
31
|
-
}
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
32
21
|
},
|
|
33
22
|
"./feature": {
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
"default": "./src/feature.ts"
|
|
37
|
-
},
|
|
38
|
-
"default": {
|
|
39
|
-
"types": "./dist/feature.d.ts",
|
|
40
|
-
"default": "./dist/feature.js"
|
|
41
|
-
}
|
|
23
|
+
"types": "./dist/feature.d.ts",
|
|
24
|
+
"default": "./dist/feature.js"
|
|
42
25
|
},
|
|
43
26
|
"./routes/*": {
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
"default": "./src/routes/*.tsx"
|
|
47
|
-
},
|
|
48
|
-
"default": {
|
|
49
|
-
"types": "./dist/routes/*.d.ts",
|
|
50
|
-
"default": "./dist/routes/*.js"
|
|
51
|
-
}
|
|
27
|
+
"types": "./dist/routes/*.d.ts",
|
|
28
|
+
"default": "./dist/routes/*.js"
|
|
52
29
|
}
|
|
53
30
|
},
|
|
54
31
|
"dependencies": {
|
|
55
32
|
"@swc/helpers": "^0.5.17",
|
|
56
33
|
"@tanstack/react-form": "^1.32.0",
|
|
57
|
-
"@shipfox/api-auth-dto": "
|
|
58
|
-
"@shipfox/api
|
|
59
|
-
"@shipfox/client-shell": "
|
|
60
|
-
"@shipfox/
|
|
61
|
-
"@shipfox/client-ui": "
|
|
62
|
-
"@shipfox/react-ui": "0.3.
|
|
34
|
+
"@shipfox/api-auth-dto": "6.0.0",
|
|
35
|
+
"@shipfox/client-api": "1.0.0",
|
|
36
|
+
"@shipfox/client-shell": "2.0.0",
|
|
37
|
+
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
38
|
+
"@shipfox/client-ui": "2.0.0",
|
|
39
|
+
"@shipfox/react-ui": "0.3.3"
|
|
63
40
|
},
|
|
64
41
|
"peerDependencies": {
|
|
65
42
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -95,12 +72,12 @@
|
|
|
95
72
|
"tailwindcss": "^4.1.13",
|
|
96
73
|
"vite": "^8.0.3",
|
|
97
74
|
"vitest": "^4.1.5",
|
|
98
|
-
"@shipfox/
|
|
99
|
-
"@shipfox/
|
|
75
|
+
"@shipfox/client-test-setup": "0.0.3",
|
|
76
|
+
"@shipfox/swc": "1.2.6",
|
|
77
|
+
"@shipfox/vitest": "1.2.3",
|
|
78
|
+
"@shipfox/typescript": "1.1.7",
|
|
100
79
|
"@shipfox/ts-config": "1.3.8",
|
|
101
|
-
"@shipfox/
|
|
102
|
-
"@shipfox/swc": "1.2.5",
|
|
103
|
-
"@shipfox/vitest": "1.2.2"
|
|
80
|
+
"@shipfox/biome": "1.8.2"
|
|
104
81
|
},
|
|
105
82
|
"scripts": {
|
|
106
83
|
"build": "shipfox-swc",
|
package/src/feature.ts
CHANGED
|
@@ -8,7 +8,6 @@ export const authFeature = defineClientFeature({
|
|
|
8
8
|
{path: '/auth/logout', parent: 'root', impl: '@shipfox/client-auth/routes/logout'},
|
|
9
9
|
{path: '/auth/reset', parent: 'root', impl: '@shipfox/client-auth/routes/reset'},
|
|
10
10
|
{path: '/auth/signup', parent: 'root', impl: '@shipfox/client-auth/routes/signup'},
|
|
11
|
-
{path: '/auth/verify-email', parent: 'root', impl: '@shipfox/client-auth/routes/verify-email'},
|
|
12
11
|
{
|
|
13
12
|
path: '/setup/workspaces/new',
|
|
14
13
|
parent: 'root',
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ export * from './pages/login-page.js';
|
|
|
8
8
|
export * from './pages/logout-page.js';
|
|
9
9
|
export * from './pages/password-reset-page.js';
|
|
10
10
|
export * from './pages/signup-page.js';
|
|
11
|
-
export * from './pages/verify-email-page.js';
|
|
12
11
|
export * from './pages/workspace-onboarding-page.js';
|
|
13
12
|
export * from './state/auth.js';
|
|
14
13
|
export {
|
|
@@ -1,19 +1,9 @@
|
|
|
1
|
-
import {EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS} from '@shipfox/api-auth-dto';
|
|
2
1
|
import {
|
|
3
|
-
getLocalResendAvailableAt,
|
|
4
2
|
getResendRemainingSeconds,
|
|
5
3
|
parseNextResendAvailableAt,
|
|
6
4
|
} from './email-verification-resend-model.js';
|
|
7
5
|
|
|
8
6
|
describe('email verification resend model', () => {
|
|
9
|
-
test('creates local cooldown targets from the shared cooldown', () => {
|
|
10
|
-
const now = Date.UTC(2026, 4, 11, 12, 0, 0);
|
|
11
|
-
|
|
12
|
-
const result = getLocalResendAvailableAt(now);
|
|
13
|
-
|
|
14
|
-
expect(result).toBe(now + EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS * 1000);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
7
|
test('rounds remaining cooldown seconds up for display', () => {
|
|
18
8
|
const now = Date.UTC(2026, 4, 11, 12, 0, 0);
|
|
19
9
|
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import {EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS} from '@shipfox/api-auth-dto';
|
|
2
|
-
|
|
3
|
-
export function getLocalResendAvailableAt(now: number): number {
|
|
4
|
-
return now + EMAIL_VERIFICATION_RESEND_COOLDOWN_SECONDS * 1000;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
1
|
export function getResendRemainingSeconds(params: {
|
|
8
2
|
nextResendAvailableAt: number | undefined;
|
|
9
3
|
now: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
-
import {act, fireEvent, screen} from '@testing-library/react';
|
|
2
|
+
import {act, fireEvent, screen, waitFor} from '@testing-library/react';
|
|
3
3
|
import {pageUserFactory} from '#test/factories/user.js';
|
|
4
4
|
import {renderAuthPage} from '#test/pages.js';
|
|
5
5
|
import {jsonResponse, requestUrl} from '#test/utils.js';
|
|
@@ -7,6 +7,12 @@ import {SignupPage} from './signup-page.js';
|
|
|
7
7
|
|
|
8
8
|
const SUBMITTED_EMAIL_RE = /new@example.com/;
|
|
9
9
|
const RESEND_COUNTDOWN_RE = /^Resend in \d+s$/;
|
|
10
|
+
function emailChallenge() {
|
|
11
|
+
return {
|
|
12
|
+
id: '019f814f-3cfd-779a-82f2-6588eefd572c',
|
|
13
|
+
next_resend_available_at: new Date(Date.now() + 60_000).toISOString(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
10
16
|
|
|
11
17
|
describe('SignupPage', () => {
|
|
12
18
|
beforeEach(() => {
|
|
@@ -24,7 +30,7 @@ describe('SignupPage', () => {
|
|
|
24
30
|
}
|
|
25
31
|
if (url === 'https://api.example.test/auth/signup' && input instanceof Request) {
|
|
26
32
|
signupBody = await input.json();
|
|
27
|
-
return jsonResponse({user}, {status: 201});
|
|
33
|
+
return jsonResponse({user, email_challenge: emailChallenge()}, {status: 201});
|
|
28
34
|
}
|
|
29
35
|
throw new Error(`Unexpected request: ${url}`);
|
|
30
36
|
});
|
|
@@ -51,6 +57,50 @@ describe('SignupPage', () => {
|
|
|
51
57
|
expect(screen.getByRole('link', {name: 'Log in'})).toHaveAttribute('href', '/auth/login');
|
|
52
58
|
});
|
|
53
59
|
|
|
60
|
+
test('submits the eight-digit verification code with its signup challenge', async () => {
|
|
61
|
+
const user = pageUserFactory.build({email: 'new@example.com', name: 'New User'});
|
|
62
|
+
let confirmBody: unknown;
|
|
63
|
+
const session = {token: 'access-token', user};
|
|
64
|
+
const fetchImpl = vi.fn().mockImplementation(async (input: RequestInfo | URL) => {
|
|
65
|
+
const url = requestUrl(input);
|
|
66
|
+
if (url === 'https://api.example.test/auth/me') {
|
|
67
|
+
return jsonResponse({code: 'unauthorized', message: 'Unauthorized'}, {status: 401});
|
|
68
|
+
}
|
|
69
|
+
if (url === 'https://api.example.test/auth/signup') {
|
|
70
|
+
return jsonResponse({user, email_challenge: emailChallenge()}, {status: 201});
|
|
71
|
+
}
|
|
72
|
+
if (
|
|
73
|
+
url === 'https://api.example.test/auth/verify-email/confirm' &&
|
|
74
|
+
input instanceof Request
|
|
75
|
+
) {
|
|
76
|
+
confirmBody = await input.json();
|
|
77
|
+
return jsonResponse(session);
|
|
78
|
+
}
|
|
79
|
+
if (url === 'https://api.example.test/auth/refresh') {
|
|
80
|
+
return jsonResponse(session);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Unexpected request: ${url}`);
|
|
83
|
+
});
|
|
84
|
+
configureApiClient({fetchImpl});
|
|
85
|
+
|
|
86
|
+
renderAuthPage('/auth/signup', <SignupPage />);
|
|
87
|
+
fireEvent.change(await screen.findByLabelText('Name'), {target: {value: 'New User'}});
|
|
88
|
+
fireEvent.change(screen.getByLabelText('Email'), {target: {value: user.email}});
|
|
89
|
+
fireEvent.change(screen.getByLabelText('Password'), {target: {value: 'long secure password'}});
|
|
90
|
+
fireEvent.click(screen.getByRole('button', {name: 'Create account'}));
|
|
91
|
+
await screen.findByRole('heading', {name: 'Check your email'});
|
|
92
|
+
fireEvent.change(screen.getByLabelText('Verification code'), {target: {value: '12345678'}});
|
|
93
|
+
fireEvent.click(screen.getByRole('button', {name: 'Verify email'}));
|
|
94
|
+
|
|
95
|
+
await waitFor(() => {
|
|
96
|
+
expect(confirmBody).toEqual({
|
|
97
|
+
email: user.email,
|
|
98
|
+
challenge_id: emailChallenge().id,
|
|
99
|
+
code: '12345678',
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
54
104
|
test('keeps credentials when switching to login', async () => {
|
|
55
105
|
const fetchImpl = vi
|
|
56
106
|
.fn()
|
|
@@ -179,7 +229,9 @@ describe('SignupPage', () => {
|
|
|
179
229
|
.mockResolvedValueOnce(
|
|
180
230
|
jsonResponse({code: 'unauthorized', message: 'Unauthorized'}, {status: 401}),
|
|
181
231
|
)
|
|
182
|
-
.mockResolvedValueOnce(
|
|
232
|
+
.mockResolvedValueOnce(
|
|
233
|
+
jsonResponse({user, email_challenge: emailChallenge()}, {status: 201}),
|
|
234
|
+
);
|
|
183
235
|
configureApiClient({fetchImpl});
|
|
184
236
|
|
|
185
237
|
renderAuthPage('/auth/signup', <SignupPage />);
|
|
@@ -211,7 +263,7 @@ describe('SignupPage', () => {
|
|
|
211
263
|
.mockResolvedValueOnce(
|
|
212
264
|
jsonResponse({code: 'unauthorized', message: 'Unauthorized'}, {status: 401}),
|
|
213
265
|
)
|
|
214
|
-
.mockResolvedValueOnce(jsonResponse({user}, {status: 201}))
|
|
266
|
+
.mockResolvedValueOnce(jsonResponse({user, email_challenge: emailChallenge()}, {status: 201}))
|
|
215
267
|
.mockResolvedValueOnce(
|
|
216
268
|
jsonResponse({next_resend_available_at: nextResendAvailableAt}, {status: 200}),
|
|
217
269
|
);
|
|
@@ -245,7 +297,7 @@ describe('SignupPage', () => {
|
|
|
245
297
|
.mockResolvedValueOnce(
|
|
246
298
|
jsonResponse({code: 'unauthorized', message: 'Unauthorized'}, {status: 401}),
|
|
247
299
|
)
|
|
248
|
-
.mockResolvedValueOnce(jsonResponse({user}, {status: 201}))
|
|
300
|
+
.mockResolvedValueOnce(jsonResponse({user, email_challenge: emailChallenge()}, {status: 201}))
|
|
249
301
|
.mockResolvedValueOnce(
|
|
250
302
|
jsonResponse({code: 'server-error', message: 'Server error'}, {status: 500}),
|
|
251
303
|
);
|
|
@@ -273,7 +325,9 @@ describe('SignupPage', () => {
|
|
|
273
325
|
.mockResolvedValueOnce(
|
|
274
326
|
jsonResponse({code: 'unauthorized', message: 'Unauthorized'}, {status: 401}),
|
|
275
327
|
)
|
|
276
|
-
.mockResolvedValueOnce(
|
|
328
|
+
.mockResolvedValueOnce(
|
|
329
|
+
jsonResponse({user, email_challenge: emailChallenge()}, {status: 201}),
|
|
330
|
+
);
|
|
277
331
|
configureApiClient({fetchImpl});
|
|
278
332
|
|
|
279
333
|
renderAuthPage('/auth/signup', <SignupPage />);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {signupBodySchema} from '@shipfox/api-auth-dto';
|
|
1
|
+
import {signupBodySchema, verifyEmailConfirmBodySchema} 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';
|
|
@@ -13,10 +13,9 @@ import {useEffect, useRef, useState} from 'react';
|
|
|
13
13
|
import {AuthShell} from '#/components/auth-shell.js';
|
|
14
14
|
import {useRefreshAuth} from '#hooks/api/refresh-auth.js';
|
|
15
15
|
import {useSignupAuth} from '#hooks/api/signup-auth.js';
|
|
16
|
-
import {useResendEmailVerificationAuth} from '#hooks/api/verify-email-auth.js';
|
|
16
|
+
import {useResendEmailVerificationAuth, useVerifyEmailAuth} from '#hooks/api/verify-email-auth.js';
|
|
17
17
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
18
18
|
import {
|
|
19
|
-
getLocalResendAvailableAt,
|
|
20
19
|
getResendRemainingSeconds,
|
|
21
20
|
parseNextResendAvailableAt,
|
|
22
21
|
} from './email-verification-resend-model.js';
|
|
@@ -30,6 +29,7 @@ import {
|
|
|
30
29
|
|
|
31
30
|
export function SignupPage() {
|
|
32
31
|
const signup = useSignupAuth();
|
|
32
|
+
const verifyEmail = useVerifyEmailAuth();
|
|
33
33
|
const resendEmailVerification = useResendEmailVerificationAuth();
|
|
34
34
|
const refreshAuth = useRefreshAuth();
|
|
35
35
|
const navigate = useNavigate();
|
|
@@ -38,7 +38,7 @@ export function SignupPage() {
|
|
|
38
38
|
const invitationPreview = useInvitationContext(invitationToken);
|
|
39
39
|
const invitationPending = pendingInvitation(invitationPreview.data);
|
|
40
40
|
const [authFormDraft, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
41
|
-
const [
|
|
41
|
+
const [emailChallenge, setEmailChallenge] = useState<{email: string; id: string} | undefined>();
|
|
42
42
|
const [now, setNow] = useState(() => Date.now());
|
|
43
43
|
const [nextResendAvailableAt, setNextResendAvailableAt] = useState<number | undefined>();
|
|
44
44
|
const [formError, setFormError] = useState<string | undefined>();
|
|
@@ -90,9 +90,16 @@ export function SignupPage() {
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
if (!result.email_challenge) {
|
|
94
|
+
throw new Error('Signup did not return an email verification challenge');
|
|
95
|
+
}
|
|
96
|
+
setEmailChallenge({email: result.user.email, id: result.email_challenge.id});
|
|
97
|
+
verificationForm.reset();
|
|
94
98
|
setResendError(undefined);
|
|
95
|
-
|
|
99
|
+
setNow(Date.now());
|
|
100
|
+
setNextResendAvailableAt(
|
|
101
|
+
parseNextResendAvailableAt(result.email_challenge.next_resend_available_at),
|
|
102
|
+
);
|
|
96
103
|
} catch (error) {
|
|
97
104
|
const mapped = signupErrorToFormError(error);
|
|
98
105
|
if (mapped.kind === 'field') {
|
|
@@ -107,8 +114,29 @@ export function SignupPage() {
|
|
|
107
114
|
},
|
|
108
115
|
});
|
|
109
116
|
|
|
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
|
+
|
|
110
138
|
useEffect(() => {
|
|
111
|
-
if (!
|
|
139
|
+
if (!emailChallenge || !nextResendAvailableAt) {
|
|
112
140
|
return;
|
|
113
141
|
}
|
|
114
142
|
|
|
@@ -126,13 +154,7 @@ export function SignupPage() {
|
|
|
126
154
|
}
|
|
127
155
|
}, 1000);
|
|
128
156
|
return () => window.clearInterval(handle);
|
|
129
|
-
}, [
|
|
130
|
-
|
|
131
|
-
function restartLocalCooldown() {
|
|
132
|
-
const current = Date.now();
|
|
133
|
-
setNow(current);
|
|
134
|
-
setNextResendAvailableAt(getLocalResendAvailableAt(current));
|
|
135
|
-
}
|
|
157
|
+
}, [emailChallenge, nextResendAvailableAt]);
|
|
136
158
|
|
|
137
159
|
// When arriving from an invitation link, prefill the email and lock it.
|
|
138
160
|
useEffect(() => {
|
|
@@ -156,17 +178,18 @@ export function SignupPage() {
|
|
|
156
178
|
}, [form, setAuthFormDraft]);
|
|
157
179
|
|
|
158
180
|
async function onResendVerificationEmail() {
|
|
159
|
-
if (!
|
|
181
|
+
if (!emailChallenge || isResendCoolingDown || resendEmailVerification.isPending) return;
|
|
160
182
|
|
|
161
183
|
setResendError(undefined);
|
|
162
184
|
try {
|
|
163
|
-
const result = await resendEmailVerification.mutateAsync({
|
|
185
|
+
const result = await resendEmailVerification.mutateAsync({
|
|
186
|
+
email: emailChallenge.email,
|
|
187
|
+
challenge_id: emailChallenge.id,
|
|
188
|
+
});
|
|
164
189
|
const nextAvailableAt = parseNextResendAvailableAt(result.next_resend_available_at);
|
|
165
190
|
setNow(Date.now());
|
|
166
191
|
if (nextAvailableAt !== undefined) {
|
|
167
192
|
setNextResendAvailableAt(nextAvailableAt);
|
|
168
|
-
} else {
|
|
169
|
-
restartLocalCooldown();
|
|
170
193
|
}
|
|
171
194
|
toast.success('If another verification email can be sent, it will arrive shortly.');
|
|
172
195
|
} catch (error) {
|
|
@@ -174,21 +197,49 @@ export function SignupPage() {
|
|
|
174
197
|
}
|
|
175
198
|
}
|
|
176
199
|
|
|
177
|
-
if (
|
|
200
|
+
if (emailChallenge) {
|
|
178
201
|
return (
|
|
179
202
|
<AuthShell
|
|
180
203
|
title="Check your email"
|
|
181
|
-
description={`We sent
|
|
204
|
+
description={`We sent an eight-digit verification code to ${emailChallenge.email}.`}
|
|
182
205
|
>
|
|
183
206
|
{resendError ? (
|
|
184
207
|
<Callout role="alert" type="error">
|
|
185
208
|
{resendError}
|
|
186
209
|
</Callout>
|
|
187
210
|
) : null}
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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>
|
|
192
243
|
<Button
|
|
193
244
|
aria-disabled={isResendCoolingDown ? true : undefined}
|
|
194
245
|
className="w-full aria-disabled:cursor-not-allowed aria-disabled:opacity-70"
|
|
@@ -203,18 +254,18 @@ export function SignupPage() {
|
|
|
203
254
|
? `Resend in ${resendRemainingSeconds}s`
|
|
204
255
|
: 'Resend verification email'}
|
|
205
256
|
</Button>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
</
|
|
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>
|
|
218
269
|
<Text size="sm" className="text-center text-foreground-neutral-subtle">
|
|
219
270
|
Already verified?{' '}
|
|
220
271
|
<ButtonLink asChild variant="interactive" underline>
|
package/test/pages.tsx
CHANGED
|
@@ -13,7 +13,6 @@ import {AuthProvider} from '#components/auth-provider.js';
|
|
|
13
13
|
import {LoginPage} from '#pages/login-page.js';
|
|
14
14
|
import {PasswordResetPage} from '#pages/password-reset-page.js';
|
|
15
15
|
import {SignupPage} from '#pages/signup-page.js';
|
|
16
|
-
import {VerifyEmailPage} from '#pages/verify-email-page.js';
|
|
17
16
|
|
|
18
17
|
function createTestRouter(path: string, element: ReactElement) {
|
|
19
18
|
const rootRoute = createRootRoute({
|
|
@@ -44,18 +43,12 @@ function createTestRouter(path: string, element: ReactElement) {
|
|
|
44
43
|
path: '/auth/reset',
|
|
45
44
|
component: () => (path.split('?')[0] === '/auth/reset' ? element : <PasswordResetPage />),
|
|
46
45
|
});
|
|
47
|
-
const verifyEmailRoute = createRoute({
|
|
48
|
-
getParentRoute: () => rootRoute,
|
|
49
|
-
path: '/auth/verify-email',
|
|
50
|
-
component: () => (path.split('?')[0] === '/auth/verify-email' ? element : <VerifyEmailPage />),
|
|
51
|
-
});
|
|
52
46
|
const routeTree = rootRoute.addChildren([
|
|
53
47
|
indexRoute,
|
|
54
48
|
workspaceRoute,
|
|
55
49
|
loginRoute,
|
|
56
50
|
signupRoute,
|
|
57
51
|
passwordResetRoute,
|
|
58
|
-
verifyEmailRoute,
|
|
59
52
|
]);
|
|
60
53
|
return createRouter({
|
|
61
54
|
history: createMemoryHistory({initialEntries: [path]}),
|