@shipfox/client-auth 3.0.1 → 5.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 +32 -0
- 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/components/workspace-switcher.stories.d.ts +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/auth-mapper.d.ts +21 -0
- package/dist/hooks/api/auth-mapper.d.ts.map +1 -0
- package/dist/hooks/api/auth-mapper.js +25 -0
- package/dist/hooks/api/auth-mapper.js.map +1 -0
- package/dist/hooks/api/login-auth.d.ts +3 -25
- package/dist/hooks/api/login-auth.d.ts.map +1 -1
- package/dist/hooks/api/login-auth.js +9 -30
- 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 +3 -25
- package/dist/hooks/api/password-reset-auth.d.ts.map +1 -1
- package/dist/hooks/api/password-reset-auth.js +9 -25
- package/dist/hooks/api/password-reset-auth.js.map +1 -1
- package/dist/hooks/api/signup-auth.d.ts +1 -25
- package/dist/hooks/api/signup-auth.d.ts.map +1 -1
- package/dist/hooks/api/signup-auth.js +5 -2
- package/dist/hooks/api/signup-auth.js.map +1 -1
- package/dist/hooks/api/verify-email-auth.d.ts +1 -12
- package/dist/hooks/api/verify-email-auth.d.ts.map +1 -1
- package/dist/hooks/api/verify-email-auth.js +10 -26
- package/dist/hooks/api/verify-email-auth.js.map +1 -1
- package/dist/hooks/api/workspace-auth.d.ts +3 -17
- package/dist/hooks/api/workspace-auth.d.ts.map +1 -1
- package/dist/hooks/api/workspace-auth.js +5 -2
- package/dist/hooks/api/workspace-auth.js.map +1 -1
- package/dist/hooks/api/workspace-mapper.d.ts +8 -0
- package/dist/hooks/api/workspace-mapper.d.ts.map +1 -0
- package/dist/hooks/api/workspace-mapper.js +9 -0
- package/dist/hooks/api/workspace-mapper.js.map +1 -0
- 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/invitation-context.d.ts +9 -19
- package/dist/pages/invitation-context.d.ts.map +1 -1
- package/dist/pages/invitation-context.js +7 -46
- package/dist/pages/invitation-context.js.map +1 -1
- package/dist/pages/login-page.js +2 -2
- package/dist/pages/login-page.js.map +1 -1
- package/dist/pages/logout-page.js +1 -1
- package/dist/pages/logout-page.js.map +1 -1
- package/dist/pages/password-reset-page.d.ts.map +1 -1
- package/dist/pages/password-reset-page.js +1 -4
- package/dist/pages/password-reset-page.js.map +1 -1
- package/dist/pages/signup-page.d.ts.map +1 -1
- package/dist/pages/signup-page.js +58 -152
- 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 +10 -39
- package/src/components/auth-provider.test.tsx +173 -3
- 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/auth-mapper.test.ts +77 -0
- package/src/hooks/api/auth-mapper.ts +39 -0
- package/src/hooks/api/login-auth.test.ts +1 -1
- package/src/hooks/api/login-auth.ts +12 -31
- package/src/hooks/api/logout-auth.ts +4 -10
- package/src/hooks/api/password-reset-auth.test.ts +1 -1
- package/src/hooks/api/password-reset-auth.ts +19 -32
- package/src/hooks/api/signup-auth.ts +8 -3
- package/src/hooks/api/verify-email-auth.ts +21 -34
- package/src/hooks/api/workspace-auth.ts +8 -3
- package/src/hooks/api/workspace-mapper.test.ts +21 -0
- package/src/hooks/api/workspace-mapper.ts +11 -0
- package/src/index.ts +0 -1
- package/src/pages/invitation-context.ts +10 -51
- package/src/pages/login-page.test.tsx +1 -1
- package/src/pages/login-page.tsx +2 -2
- package/src/pages/logout-page.tsx +1 -1
- package/src/pages/password-reset-page.tsx +1 -4
- package/src/pages/signup-page.test.tsx +1 -1
- package/src/pages/signup-page.tsx +41 -137
- package/src/state/auth.ts +1 -0
- package/src/state/last-workspace.test.ts +16 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/components/auth-shell.d.ts +0 -11
- package/dist/components/auth-shell.d.ts.map +0 -1
- package/dist/components/auth-shell.js +0 -63
- package/dist/components/auth-shell.js.map +0 -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/components/auth-shell.tsx +0 -53
- package/src/pages/email-verification-resend-model.test.ts +0 -42
- package/src/pages/email-verification-resend-model.ts +0 -15
|
@@ -1,57 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type InvitationPreview,
|
|
3
|
+
pendingInvitation,
|
|
4
|
+
usePreviewInvitation,
|
|
5
|
+
} from '@shipfox/client-invitations';
|
|
6
|
+
import {parseRedirectContext} from '#/components/redirect-context.js';
|
|
7
7
|
|
|
8
|
-
const INVITATION_ACCEPT_PATH = '/invitations/accept';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Extract an invitation token from a `redirect=` URL if it points at the
|
|
12
|
-
* canonical pre-auth invitation page. Returns undefined when the redirect is
|
|
13
|
-
* absent, malformed, or unrelated to invitations.
|
|
14
|
-
*/
|
|
15
8
|
export function extractInvitationToken(redirect: unknown): string | undefined {
|
|
16
|
-
|
|
17
|
-
let decoded: string;
|
|
18
|
-
try {
|
|
19
|
-
decoded = decodeURIComponent(redirect);
|
|
20
|
-
} catch {
|
|
21
|
-
return undefined;
|
|
22
|
-
}
|
|
23
|
-
if (!decoded.startsWith('/')) return undefined;
|
|
24
|
-
const [path, queryString = ''] = decoded.split('?', 2);
|
|
25
|
-
if (path !== INVITATION_ACCEPT_PATH) return undefined;
|
|
26
|
-
const params = new URLSearchParams(queryString);
|
|
27
|
-
const token = params.get('token');
|
|
28
|
-
return token && token.length > 0 ? token : undefined;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
async function fetchPreview(token: string): Promise<PreviewInvitationResponseDto> {
|
|
32
|
-
const params = new URLSearchParams({token});
|
|
33
|
-
return await apiRequest<PreviewInvitationResponseDto>(
|
|
34
|
-
`/invitations/preview?${params.toString()}`,
|
|
35
|
-
);
|
|
9
|
+
return parseRedirectContext(redirect).invitationToken;
|
|
36
10
|
}
|
|
37
11
|
|
|
38
|
-
export function pendingInvitation(
|
|
39
|
-
data: PreviewInvitationResponseDto | undefined,
|
|
40
|
-
): PreviewInvitationPendingDto | undefined {
|
|
41
|
-
return data?.status === 'pending' ? data : undefined;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Mirrors the hook in `@shipfox/client-invitations` but lives in `client-auth`
|
|
46
|
-
* to avoid a circular dependency. Signup/login pages call this to lock the
|
|
47
|
-
* email field when arriving from an invitation link.
|
|
48
|
-
*/
|
|
49
12
|
export function useInvitationContext(token: string | undefined) {
|
|
50
|
-
return
|
|
51
|
-
queryKey: ['invitations', 'preview', token ?? ''] as const,
|
|
52
|
-
queryFn: () => fetchPreview(token as string),
|
|
53
|
-
enabled: Boolean(token),
|
|
54
|
-
retry: false,
|
|
55
|
-
staleTime: 30_000,
|
|
56
|
-
});
|
|
13
|
+
return usePreviewInvitation(token);
|
|
57
14
|
}
|
|
15
|
+
|
|
16
|
+
export {type InvitationPreview, pendingInvitation};
|
|
@@ -70,7 +70,7 @@ describe('LoginPage', () => {
|
|
|
70
70
|
return Promise.resolve(
|
|
71
71
|
jsonResponse({
|
|
72
72
|
status: 'pending',
|
|
73
|
-
workspace_id: '
|
|
73
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
74
74
|
workspace_name: 'Invite Workspace',
|
|
75
75
|
email: 'invitee@example.com',
|
|
76
76
|
invited_by_display: 'owner@example.com',
|
package/src/pages/login-page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {loginBodySchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
2
3
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
3
4
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
4
5
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
@@ -8,7 +9,6 @@ import {useForm} from '@tanstack/react-form';
|
|
|
8
9
|
import {Link, useSearch} from '@tanstack/react-router';
|
|
9
10
|
import {useAtom} from 'jotai';
|
|
10
11
|
import {useEffect, useRef, useState} from 'react';
|
|
11
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
12
12
|
import {useLoginAuth} from '#hooks/api/login-auth.js';
|
|
13
13
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
14
14
|
import {loginErrorToFormError} from './form-errors.js';
|
|
@@ -81,7 +81,7 @@ export function LoginPage() {
|
|
|
81
81
|
? `/invitations/accept?token=${encodeURIComponent(invitationToken)}`
|
|
82
82
|
: undefined;
|
|
83
83
|
const headerTitle = invitationPending
|
|
84
|
-
? `Join ${invitationPending.
|
|
84
|
+
? `Join ${invitationPending.workspaceName}`
|
|
85
85
|
: 'Connect to Shipfox';
|
|
86
86
|
const headerDescription = invitationPending
|
|
87
87
|
? 'Log in to accept your invitation.'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
1
2
|
import {Text} from '@shipfox/react-ui/typography';
|
|
2
3
|
import {useNavigate, useRouter, useSearch} from '@tanstack/react-router';
|
|
3
4
|
import {useEffect} from 'react';
|
|
4
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
5
5
|
import {sanitizeRedirectPath} from '#/components/redirect-target.js';
|
|
6
6
|
import {useLogoutAuth} from '#hooks/api/logout-auth.js';
|
|
7
7
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
passwordResetConfirmBodySchema,
|
|
3
3
|
passwordResetRequestBodySchema,
|
|
4
4
|
} from '@shipfox/api-auth-dto';
|
|
5
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
5
6
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
6
7
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
7
8
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
@@ -11,12 +12,10 @@ import {useForm} from '@tanstack/react-form';
|
|
|
11
12
|
import {Link, useNavigate, useSearch} from '@tanstack/react-router';
|
|
12
13
|
import {useAtom} from 'jotai';
|
|
13
14
|
import {useEffect, useRef, useState} from 'react';
|
|
14
|
-
import {AuthShell} from '#/components/auth-shell.js';
|
|
15
15
|
import {
|
|
16
16
|
useConfirmPasswordResetAuth,
|
|
17
17
|
useRequestPasswordResetAuth,
|
|
18
18
|
} from '#hooks/api/password-reset-auth.js';
|
|
19
|
-
import {useRefreshAuth} from '#hooks/api/refresh-auth.js';
|
|
20
19
|
import {authFormDraftAtom, initialAuthFormDraft} from '#state/auth.js';
|
|
21
20
|
import {
|
|
22
21
|
passwordResetConfirmErrorToFormError,
|
|
@@ -148,7 +147,6 @@ function PasswordResetRequest() {
|
|
|
148
147
|
|
|
149
148
|
function PasswordResetConfirm({token}: {token: string}) {
|
|
150
149
|
const confirmPasswordReset = useConfirmPasswordResetAuth();
|
|
151
|
-
const refreshAuth = useRefreshAuth();
|
|
152
150
|
const navigate = useNavigate();
|
|
153
151
|
const [, setAuthFormDraft] = useAtom(authFormDraftAtom);
|
|
154
152
|
const [formError, setFormError] = useState<string | undefined>();
|
|
@@ -163,7 +161,6 @@ function PasswordResetConfirm({token}: {token: string}) {
|
|
|
163
161
|
new_password: value.new_password,
|
|
164
162
|
});
|
|
165
163
|
await confirmPasswordReset.mutateAsync(body);
|
|
166
|
-
await refreshAuth();
|
|
167
164
|
setAuthFormDraft(initialAuthFormDraft);
|
|
168
165
|
toast.success('Your password has been changed. You are now logged in.');
|
|
169
166
|
await navigate({to: '/', replace: true});
|
|
@@ -137,7 +137,7 @@ describe('SignupPage', () => {
|
|
|
137
137
|
return Promise.resolve(
|
|
138
138
|
jsonResponse({
|
|
139
139
|
status: 'pending',
|
|
140
|
-
workspace_id: '
|
|
140
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
141
141
|
workspace_name: 'Invite Workspace',
|
|
142
142
|
email: 'invitee@example.com',
|
|
143
143
|
invited_by_display: 'owner@example.com',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {signupBodySchema
|
|
1
|
+
import {signupBodySchema} from '@shipfox/api-auth-dto';
|
|
2
|
+
import {AuthShell} from '@shipfox/client-shell/runtime';
|
|
2
3
|
import {displayNameFieldError} from '@shipfox/client-ui';
|
|
3
4
|
import {Button, ButtonLink} from '@shipfox/react-ui/button';
|
|
4
5
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -10,15 +11,11 @@ import {useForm} from '@tanstack/react-form';
|
|
|
10
11
|
import {Link, useNavigate, useSearch} from '@tanstack/react-router';
|
|
11
12
|
import {useAtom} from 'jotai';
|
|
12
13
|
import {useEffect, useRef, useState} from 'react';
|
|
13
|
-
import {
|
|
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: ''},
|
|
@@ -73,16 +67,16 @@ export function SignupPage() {
|
|
|
73
67
|
// Refresh failures don't block the success message — the next API
|
|
74
68
|
// call's 401 handling will re-route the user.
|
|
75
69
|
}
|
|
76
|
-
toast.success(`You joined ${invitationPending.
|
|
70
|
+
toast.success(`You joined ${invitationPending.workspaceName}.`);
|
|
77
71
|
await navigate({
|
|
78
72
|
to: '/workspaces/$wid',
|
|
79
|
-
params: {wid: result.membership.
|
|
73
|
+
params: {wid: result.membership.workspaceId},
|
|
80
74
|
});
|
|
81
75
|
return;
|
|
82
76
|
}
|
|
83
77
|
|
|
84
|
-
if (invitationToken && result.
|
|
85
|
-
toast.error(result.
|
|
78
|
+
if (invitationToken && result.acceptError) {
|
|
79
|
+
toast.error(result.acceptError.message);
|
|
86
80
|
await navigate({
|
|
87
81
|
to: '/invitations/accept',
|
|
88
82
|
search: {token: invitationToken},
|
|
@@ -90,16 +84,12 @@ export function SignupPage() {
|
|
|
90
84
|
return;
|
|
91
85
|
}
|
|
92
86
|
|
|
93
|
-
if (!result.
|
|
87
|
+
if (!result.emailChallenge) {
|
|
94
88
|
throw new Error('Signup did not return an email verification challenge');
|
|
95
89
|
}
|
|
96
|
-
setEmailChallenge({email: result.user.email, id: result.
|
|
97
|
-
verificationForm.reset();
|
|
90
|
+
setEmailChallenge({email: result.user.email, id: result.emailChallenge.id});
|
|
98
91
|
setResendError(undefined);
|
|
99
|
-
|
|
100
|
-
setNextResendAvailableAt(
|
|
101
|
-
parseNextResendAvailableAt(result.email_challenge.next_resend_available_at),
|
|
102
|
-
);
|
|
92
|
+
setNextResendAvailableAt(result.emailChallenge.nextResendAvailableAt);
|
|
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,85 +143,45 @@ 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
|
|
|
279
183
|
const headerTitle = invitationPending
|
|
280
|
-
? `Join ${invitationPending.
|
|
184
|
+
? `Join ${invitationPending.workspaceName}`
|
|
281
185
|
: 'Create your Shipfox account';
|
|
282
186
|
const headerDescription = invitationPending
|
|
283
187
|
? `Create an account to accept your invitation.`
|
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
|
|