@shipfox/client-invitations 4.0.0 → 6.0.1
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 +47 -0
- package/dist/complete-acceptance.d.ts +1 -1
- package/dist/complete-acceptance.js +1 -1
- package/dist/complete-acceptance.js.map +1 -1
- package/dist/core/invitation-acceptance.d.ts +9 -0
- package/dist/core/invitation-acceptance.d.ts.map +1 -0
- package/dist/core/invitation-acceptance.js +3 -0
- package/dist/core/invitation-acceptance.js.map +1 -0
- package/dist/core/invitation-preview.d.ts +21 -0
- package/dist/core/invitation-preview.d.ts.map +1 -0
- package/dist/core/invitation-preview.js +5 -0
- package/dist/core/invitation-preview.js.map +1 -0
- package/dist/hooks/api/accept-invitation.d.ts +1 -8
- package/dist/hooks/api/accept-invitation.d.ts.map +1 -1
- package/dist/hooks/api/accept-invitation.js +7 -4
- package/dist/hooks/api/accept-invitation.js.map +1 -1
- package/dist/hooks/api/invitation-acceptance-mapper.d.ts +4 -0
- package/dist/hooks/api/invitation-acceptance-mapper.d.ts.map +1 -0
- package/dist/hooks/api/invitation-acceptance-mapper.js +12 -0
- package/dist/hooks/api/invitation-acceptance-mapper.js.map +1 -0
- package/dist/hooks/api/invitation-preview-mapper.d.ts +4 -0
- package/dist/hooks/api/invitation-preview-mapper.d.ts.map +1 -0
- package/dist/hooks/api/invitation-preview-mapper.js +32 -0
- package/dist/hooks/api/invitation-preview-mapper.js.map +1 -0
- package/dist/hooks/api/preview-invitation.d.ts +12 -7
- package/dist/hooks/api/preview-invitation.d.ts.map +1 -1
- package/dist/hooks/api/preview-invitation.js +12 -6
- package/dist/hooks/api/preview-invitation.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pages/invitation-accept-page.d.ts.map +1 -1
- package/dist/pages/invitation-accept-page.js +17 -19
- package/dist/pages/invitation-accept-page.js.map +1 -1
- package/dist/routes/accept.d.ts +2 -0
- package/dist/routes/accept.d.ts.map +1 -1
- package/dist/routes/accept.js +2 -0
- package/dist/routes/accept.js.map +1 -1
- package/dist/routes/inputs.d.ts +5 -0
- package/dist/routes/inputs.d.ts.map +1 -0
- package/dist/routes/inputs.js +8 -0
- package/dist/routes/inputs.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +10 -12
- package/src/complete-acceptance.ts +1 -1
- package/src/core/invitation-acceptance.ts +8 -0
- package/src/core/invitation-preview.ts +18 -0
- package/src/hooks/api/accept-invitation.ts +7 -8
- package/src/hooks/api/invitation-acceptance-mapper.test.ts +23 -0
- package/src/hooks/api/invitation-acceptance-mapper.ts +13 -0
- package/src/hooks/api/invitation-preview-mapper.test.ts +36 -0
- package/src/hooks/api/invitation-preview-mapper.ts +22 -0
- package/src/hooks/api/preview-invitation.ts +24 -9
- package/src/index.ts +3 -0
- package/src/pages/invitation-accept-page.tsx +24 -20
- package/src/routes/accept.tsx +5 -1
- package/src/routes/inputs.test.ts +11 -0
- package/src/routes/inputs.ts +10 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-invitations",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "6.0.1",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -10,10 +10,6 @@
|
|
|
10
10
|
"type": "module",
|
|
11
11
|
"main": "dist/index.js",
|
|
12
12
|
"types": "dist/index.d.ts",
|
|
13
|
-
"imports": {
|
|
14
|
-
"#*": "./dist/*",
|
|
15
|
-
"#test/*": "./test/*"
|
|
16
|
-
},
|
|
17
13
|
"exports": {
|
|
18
14
|
".": {
|
|
19
15
|
"types": "./dist/index.d.ts",
|
|
@@ -31,13 +27,12 @@
|
|
|
31
27
|
"dependencies": {
|
|
32
28
|
"@swc/helpers": "^0.5.17",
|
|
33
29
|
"@tanstack/react-query": "^5.101.0",
|
|
34
|
-
"@shipfox/api-
|
|
35
|
-
"@shipfox/
|
|
36
|
-
"@shipfox/api
|
|
37
|
-
"@shipfox/client-
|
|
38
|
-
"@shipfox/
|
|
39
|
-
"@shipfox/client-
|
|
40
|
-
"@shipfox/react-ui": "0.3.5"
|
|
30
|
+
"@shipfox/api-auth-dto": "9.0.1",
|
|
31
|
+
"@shipfox/api-workspaces-dto": "9.0.1",
|
|
32
|
+
"@shipfox/client-api": "6.0.1",
|
|
33
|
+
"@shipfox/client-ui": "6.0.1",
|
|
34
|
+
"@shipfox/react-ui": "0.3.6",
|
|
35
|
+
"@shipfox/client-shell": "6.0.1"
|
|
41
36
|
},
|
|
42
37
|
"peerDependencies": {
|
|
43
38
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -45,6 +40,9 @@
|
|
|
45
40
|
"react": "^19.0.0",
|
|
46
41
|
"react-dom": "^19.0.0"
|
|
47
42
|
},
|
|
43
|
+
"imports": {
|
|
44
|
+
"#*": "./dist/*"
|
|
45
|
+
},
|
|
48
46
|
"scripts": {
|
|
49
47
|
"build": "shipfox-swc",
|
|
50
48
|
"check": "shipfox-biome-check",
|
|
@@ -8,7 +8,7 @@ import type {NavigateOptions} from '@tanstack/react-router';
|
|
|
8
8
|
* routes the user into the workspace home.
|
|
9
9
|
*
|
|
10
10
|
* `refreshAuth` is passed in so this helper stays a plain function (no hook).
|
|
11
|
-
* Call sites construct it via `useRefreshAuth()` from `@shipfox/client-
|
|
11
|
+
* Call sites construct it via `useRefreshAuth()` from `@shipfox/client-shell/runtime`.
|
|
12
12
|
*/
|
|
13
13
|
export async function completeInvitationAcceptance(params: {
|
|
14
14
|
workspaceId: string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type InvitationPreview =
|
|
2
|
+
| {
|
|
3
|
+
status: 'pending';
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
workspaceName: string;
|
|
6
|
+
email: string;
|
|
7
|
+
invitedByDisplay?: string;
|
|
8
|
+
expiresAt: string;
|
|
9
|
+
}
|
|
10
|
+
| {status: 'expired'; workspaceName: string; expiresAt: string}
|
|
11
|
+
| {status: 'already_used'; workspaceName: string}
|
|
12
|
+
| {status: 'invalid'};
|
|
13
|
+
|
|
14
|
+
export function pendingInvitation(
|
|
15
|
+
preview: InvitationPreview | undefined,
|
|
16
|
+
): Extract<InvitationPreview, {status: 'pending'}> | undefined {
|
|
17
|
+
return preview?.status === 'pending' ? preview : undefined;
|
|
18
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
AcceptInvitationResponseDto,
|
|
4
|
-
} from '@shipfox/api-workspaces-dto';
|
|
5
|
-
import {apiRequest} from '@shipfox/client-api';
|
|
1
|
+
import {acceptInvitationResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
6
3
|
import {useMutation} from '@tanstack/react-query';
|
|
4
|
+
import {toInvitationAcceptance} from './invitation-acceptance-mapper.js';
|
|
7
5
|
|
|
8
|
-
async function acceptInvitation(
|
|
9
|
-
|
|
6
|
+
async function acceptInvitation(command: {token: string}) {
|
|
7
|
+
const response = await checkedApiRequest(acceptInvitationResponseSchema, '/invitations/accept', {
|
|
10
8
|
method: 'POST',
|
|
11
|
-
body,
|
|
9
|
+
body: command,
|
|
12
10
|
});
|
|
11
|
+
return toInvitationAcceptance(response);
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export function useAcceptInvitation() {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {toInvitationAcceptance} from './invitation-acceptance-mapper.js';
|
|
2
|
+
|
|
3
|
+
describe('toInvitationAcceptance', () => {
|
|
4
|
+
test('maps the acceptance response into the invitation domain', () => {
|
|
5
|
+
const result = toInvitationAcceptance({
|
|
6
|
+
membership: {
|
|
7
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
8
|
+
user_id: '22222222-2222-4222-8222-222222222222',
|
|
9
|
+
workspace_id: '33333333-3333-4333-8333-333333333333',
|
|
10
|
+
},
|
|
11
|
+
already_member: true,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
membership: {
|
|
16
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
17
|
+
userId: '22222222-2222-4222-8222-222222222222',
|
|
18
|
+
workspaceId: '33333333-3333-4333-8333-333333333333',
|
|
19
|
+
},
|
|
20
|
+
alreadyMember: true,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type {AcceptInvitationResponseDto} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import type {InvitationAcceptance} from '#core/invitation-acceptance.js';
|
|
3
|
+
|
|
4
|
+
export function toInvitationAcceptance(dto: AcceptInvitationResponseDto): InvitationAcceptance {
|
|
5
|
+
return {
|
|
6
|
+
membership: {
|
|
7
|
+
id: dto.membership.id,
|
|
8
|
+
userId: dto.membership.user_id,
|
|
9
|
+
workspaceId: dto.membership.workspace_id,
|
|
10
|
+
},
|
|
11
|
+
alreadyMember: dto.already_member,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {describe, expect, it} from '@shipfox/vitest/vi';
|
|
2
|
+
import {toInvitationPreview} from './invitation-preview-mapper.js';
|
|
3
|
+
|
|
4
|
+
describe('toInvitationPreview', () => {
|
|
5
|
+
it.each([
|
|
6
|
+
[
|
|
7
|
+
{
|
|
8
|
+
status: 'pending' as const,
|
|
9
|
+
workspace_id: '11111111-1111-4111-8111-111111111111',
|
|
10
|
+
workspace_name: 'Acme',
|
|
11
|
+
email: 'member@example.com',
|
|
12
|
+
invited_by_display: 'Owner',
|
|
13
|
+
expires_at: '2026-08-01T00:00:00.000Z',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
status: 'pending',
|
|
17
|
+
workspaceId: '11111111-1111-4111-8111-111111111111',
|
|
18
|
+
workspaceName: 'Acme',
|
|
19
|
+
email: 'member@example.com',
|
|
20
|
+
invitedByDisplay: 'Owner',
|
|
21
|
+
expiresAt: '2026-08-01T00:00:00.000Z',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
{status: 'expired' as const, workspace_name: 'Acme', expires_at: '2026-08-01T00:00:00.000Z'},
|
|
26
|
+
{status: 'expired', workspaceName: 'Acme', expiresAt: '2026-08-01T00:00:00.000Z'},
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
{status: 'already_used' as const, workspace_name: 'Acme'},
|
|
30
|
+
{status: 'already_used', workspaceName: 'Acme'},
|
|
31
|
+
],
|
|
32
|
+
[{status: 'invalid' as const}, {status: 'invalid'}],
|
|
33
|
+
])('maps %s previews into the invitation domain', (dto, expected) => {
|
|
34
|
+
expect(toInvitationPreview(dto)).toEqual(expected);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {PreviewInvitationResponseDto} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import type {InvitationPreview} from '#core/invitation-preview.js';
|
|
3
|
+
|
|
4
|
+
export function toInvitationPreview(dto: PreviewInvitationResponseDto): InvitationPreview {
|
|
5
|
+
switch (dto.status) {
|
|
6
|
+
case 'pending':
|
|
7
|
+
return {
|
|
8
|
+
status: dto.status,
|
|
9
|
+
workspaceId: dto.workspace_id,
|
|
10
|
+
workspaceName: dto.workspace_name,
|
|
11
|
+
email: dto.email,
|
|
12
|
+
...(dto.invited_by_display ? {invitedByDisplay: dto.invited_by_display} : {}),
|
|
13
|
+
expiresAt: dto.expires_at,
|
|
14
|
+
};
|
|
15
|
+
case 'expired':
|
|
16
|
+
return {status: dto.status, workspaceName: dto.workspace_name, expiresAt: dto.expires_at};
|
|
17
|
+
case 'already_used':
|
|
18
|
+
return {status: dto.status, workspaceName: dto.workspace_name};
|
|
19
|
+
case 'invalid':
|
|
20
|
+
return {status: dto.status};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {useQuery} from '@tanstack/react-query';
|
|
1
|
+
import {previewInvitationResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
|
+
import {type FetchQueryOptions, queryOptions, useQuery} from '@tanstack/react-query';
|
|
4
|
+
import type {InvitationPreview} from '#core/invitation-preview.js';
|
|
5
|
+
import {toInvitationPreview} from './invitation-preview-mapper.js';
|
|
4
6
|
|
|
5
7
|
export const previewInvitationQueryKey = (token: string) =>
|
|
6
8
|
['invitations', 'preview', token] as const;
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
type PreviewInvitationQueryOptions = FetchQueryOptions<
|
|
11
|
+
InvitationPreview,
|
|
12
|
+
Error,
|
|
13
|
+
InvitationPreview,
|
|
14
|
+
ReturnType<typeof previewInvitationQueryKey>
|
|
15
|
+
>;
|
|
16
|
+
|
|
17
|
+
async function previewInvitation(token: string): Promise<InvitationPreview> {
|
|
9
18
|
const params = new URLSearchParams({token});
|
|
10
|
-
|
|
19
|
+
const response = await checkedApiRequest(
|
|
20
|
+
previewInvitationResponseSchema,
|
|
11
21
|
`/invitations/preview?${params.toString()}`,
|
|
12
22
|
);
|
|
23
|
+
return toInvitationPreview(response);
|
|
13
24
|
}
|
|
14
25
|
|
|
15
|
-
export function
|
|
16
|
-
return
|
|
17
|
-
queryKey: previewInvitationQueryKey(token
|
|
18
|
-
queryFn: () => previewInvitation(token
|
|
26
|
+
export function previewInvitationQueryOptions(token: string): PreviewInvitationQueryOptions {
|
|
27
|
+
return queryOptions({
|
|
28
|
+
queryKey: previewInvitationQueryKey(token),
|
|
29
|
+
queryFn: () => previewInvitation(token),
|
|
19
30
|
enabled: Boolean(token),
|
|
20
31
|
retry: false,
|
|
21
32
|
staleTime: 30_000,
|
|
22
33
|
});
|
|
23
34
|
}
|
|
35
|
+
|
|
36
|
+
export function usePreviewInvitation(token: string | undefined) {
|
|
37
|
+
return useQuery(previewInvitationQueryOptions(token ?? ''));
|
|
38
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
export type {InvitationAcceptance} from '#core/invitation-acceptance.js';
|
|
2
|
+
export {type InvitationPreview, pendingInvitation} from '#core/invitation-preview.js';
|
|
1
3
|
export {completeInvitationAcceptance} from './complete-acceptance.js';
|
|
2
4
|
export {useAcceptInvitation} from './hooks/api/accept-invitation.js';
|
|
3
5
|
export {
|
|
4
6
|
previewInvitationQueryKey,
|
|
7
|
+
previewInvitationQueryOptions,
|
|
5
8
|
usePreviewInvitation,
|
|
6
9
|
} from './hooks/api/preview-invitation.js';
|
|
7
10
|
export {InvitationAcceptPage} from './pages/invitation-accept-page.js';
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AuthShell,
|
|
3
|
+
useAuthState,
|
|
4
|
+
useRefreshAuth,
|
|
5
|
+
useRouteSearch,
|
|
6
|
+
} from '@shipfox/client-shell/runtime';
|
|
2
7
|
import {createSingleFlight} from '@shipfox/client-ui';
|
|
3
8
|
import {Button} from '@shipfox/react-ui/button';
|
|
4
9
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -6,20 +11,19 @@ import {ShipfoxLoader} from '@shipfox/react-ui/loader';
|
|
|
6
11
|
import {toast} from '@shipfox/react-ui/toast';
|
|
7
12
|
import {Text} from '@shipfox/react-ui/typography';
|
|
8
13
|
import {formatDate} from '@shipfox/react-ui/utils';
|
|
9
|
-
import {Link, useNavigate
|
|
14
|
+
import {Link, useNavigate} from '@tanstack/react-router';
|
|
10
15
|
import {useCallback, useEffect, useRef} from 'react';
|
|
11
16
|
import {completeInvitationAcceptance} from '#complete-acceptance.js';
|
|
12
17
|
import {useAcceptInvitation} from '#hooks/api/accept-invitation.js';
|
|
13
18
|
import {usePreviewInvitation} from '#hooks/api/preview-invitation.js';
|
|
19
|
+
import {validateInvitationAcceptSearch} from '../routes/inputs.js';
|
|
14
20
|
|
|
15
21
|
const invitationAccepts = createSingleFlight<string, void>();
|
|
16
22
|
const toastedTerminals = new Set<string>();
|
|
17
23
|
const TOASTED_TERMINALS_MAX = 32;
|
|
18
24
|
|
|
19
25
|
export function InvitationAcceptPage() {
|
|
20
|
-
const
|
|
21
|
-
const token =
|
|
22
|
-
typeof search.token === 'string' && search.token.length > 0 ? search.token : undefined;
|
|
26
|
+
const {token} = useRouteSearch(validateInvitationAcceptSearch);
|
|
23
27
|
const navigate = useNavigate();
|
|
24
28
|
const auth = useAuthState();
|
|
25
29
|
const refreshAuth = useRefreshAuth();
|
|
@@ -53,7 +57,7 @@ export function InvitationAcceptPage() {
|
|
|
53
57
|
const runAccept = useCallback(
|
|
54
58
|
async (params: {token: string; workspaceName: string}) => {
|
|
55
59
|
const result = await accept.mutateAsync({token: params.token});
|
|
56
|
-
await completeAccept(result.membership.
|
|
60
|
+
await completeAccept(result.membership.workspaceId, params.workspaceName);
|
|
57
61
|
},
|
|
58
62
|
[accept, completeAccept],
|
|
59
63
|
);
|
|
@@ -74,7 +78,7 @@ export function InvitationAcceptPage() {
|
|
|
74
78
|
|
|
75
79
|
hasKickedAccept.current = true;
|
|
76
80
|
invitationAccepts
|
|
77
|
-
.run(token, async () => await runAccept({token, workspaceName: data.
|
|
81
|
+
.run(token, async () => await runAccept({token, workspaceName: data.workspaceName}))
|
|
78
82
|
.catch(() => {
|
|
79
83
|
// The mutation surfaces its error via accept.error; the UI re-renders
|
|
80
84
|
// into the auth-match-error state below.
|
|
@@ -137,7 +141,7 @@ export function InvitationAcceptPage() {
|
|
|
137
141
|
return (
|
|
138
142
|
<AuthShell title="Invitation expired" description="The link is no longer accepting joins.">
|
|
139
143
|
<Callout role="alert" type="error">
|
|
140
|
-
This invitation expired on {formatDate(data.
|
|
144
|
+
This invitation expired on {formatDate(data.expiresAt)}. Ask your administrator to send a
|
|
141
145
|
new one.
|
|
142
146
|
</Callout>
|
|
143
147
|
<GoHomeButton navigate={navigate} />
|
|
@@ -150,12 +154,12 @@ export function InvitationAcceptPage() {
|
|
|
150
154
|
toast.info(`This invitation has already been accepted.`),
|
|
151
155
|
);
|
|
152
156
|
return (
|
|
153
|
-
<AuthShell title="Invitation already used" description={data.
|
|
157
|
+
<AuthShell title="Invitation already used" description={data.workspaceName}>
|
|
154
158
|
<Callout role="alert" type="info">
|
|
155
159
|
This invitation has already been accepted.{' '}
|
|
156
160
|
{auth.isAuthenticated
|
|
157
|
-
? `Open your dashboard to access ${data.
|
|
158
|
-
: `Log in to access ${data.
|
|
161
|
+
? `Open your dashboard to access ${data.workspaceName} if your account is a member.`
|
|
162
|
+
: `Log in to access ${data.workspaceName}.`}
|
|
159
163
|
</Callout>
|
|
160
164
|
{auth.isAuthenticated ? null : (
|
|
161
165
|
<Button asChild className="w-full">
|
|
@@ -168,8 +172,8 @@ export function InvitationAcceptPage() {
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
const inviterLine =
|
|
171
|
-
data.
|
|
172
|
-
? `Invited by ${data.
|
|
175
|
+
data.invitedByDisplay != null
|
|
176
|
+
? `Invited by ${data.invitedByDisplay} to join as ${data.email}.`
|
|
173
177
|
: `Invited to join as ${data.email}.`;
|
|
174
178
|
|
|
175
179
|
if (!auth.isAuthenticated) {
|
|
@@ -177,7 +181,7 @@ export function InvitationAcceptPage() {
|
|
|
177
181
|
const signupHref = `/auth/signup?redirect=${encodeURIComponent(redirect)}`;
|
|
178
182
|
const loginHref = `/auth/login?redirect=${encodeURIComponent(redirect)}`;
|
|
179
183
|
return (
|
|
180
|
-
<AuthShell title={data.
|
|
184
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
181
185
|
<div className="flex flex-col gap-16">
|
|
182
186
|
<Button asChild className="w-full">
|
|
183
187
|
<Link to={signupHref}>Create account</Link>
|
|
@@ -199,7 +203,7 @@ export function InvitationAcceptPage() {
|
|
|
199
203
|
const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;
|
|
200
204
|
const logoutHref = `/auth/logout?redirect=${encodeURIComponent(redirect)}`;
|
|
201
205
|
return (
|
|
202
|
-
<AuthShell title={data.
|
|
206
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
203
207
|
<Callout role="alert" type="warning">
|
|
204
208
|
You're signed in as {auth.user?.email}, but this invitation is for {data.email}.
|
|
205
209
|
</Callout>
|
|
@@ -223,15 +227,15 @@ export function InvitationAcceptPage() {
|
|
|
223
227
|
// result. Show either the pending state or the error state.
|
|
224
228
|
if (accept.isError) {
|
|
225
229
|
return (
|
|
226
|
-
<AuthShell title={data.
|
|
230
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
227
231
|
<Callout role="alert" type="error">
|
|
228
|
-
We couldn't add you to {data.
|
|
232
|
+
We couldn't add you to {data.workspaceName}. Try again in a moment.
|
|
229
233
|
</Callout>
|
|
230
234
|
<Button
|
|
231
235
|
className="w-full"
|
|
232
236
|
isLoading={accept.isPending}
|
|
233
237
|
onClick={() => {
|
|
234
|
-
runAccept({token, workspaceName: data.
|
|
238
|
+
runAccept({token, workspaceName: data.workspaceName}).catch(() => {
|
|
235
239
|
// The mutation state drives the rendered error; avoid an
|
|
236
240
|
// unhandled rejection from the click handler.
|
|
237
241
|
});
|
|
@@ -244,11 +248,11 @@ export function InvitationAcceptPage() {
|
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
return (
|
|
247
|
-
<AuthShell title={data.
|
|
251
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
248
252
|
<div className="flex flex-col items-center gap-12" aria-live="polite">
|
|
249
253
|
<ShipfoxLoader />
|
|
250
254
|
<Text size="sm" className="text-foreground-neutral-subtle">
|
|
251
|
-
Adding you to {data.
|
|
255
|
+
Adding you to {data.workspaceName}…
|
|
252
256
|
</Text>
|
|
253
257
|
</div>
|
|
254
258
|
</AuthShell>
|
package/src/routes/accept.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
2
2
|
import {InvitationAcceptPage} from '#pages/invitation-accept-page.js';
|
|
3
|
+
import {validateInvitationAcceptSearch} from './inputs.js';
|
|
3
4
|
|
|
4
|
-
export default defineRoute({
|
|
5
|
+
export default defineRoute({
|
|
6
|
+
validateSearch: validateInvitationAcceptSearch,
|
|
7
|
+
component: InvitationAcceptPage,
|
|
8
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {validateInvitationAcceptSearch} from './inputs.js';
|
|
2
|
+
|
|
3
|
+
describe('invitation route inputs', () => {
|
|
4
|
+
it('accepts a token and drops missing or malformed values', () => {
|
|
5
|
+
expect(validateInvitationAcceptSearch({token: 'invite-token'})).toEqual({
|
|
6
|
+
token: 'invite-token',
|
|
7
|
+
});
|
|
8
|
+
expect(validateInvitationAcceptSearch({})).toEqual({});
|
|
9
|
+
expect(validateInvitationAcceptSearch({token: ['invite-token']})).toEqual({});
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface InvitationAcceptSearch {
|
|
2
|
+
token?: string;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function validateInvitationAcceptSearch(
|
|
6
|
+
input: Record<string, unknown>,
|
|
7
|
+
): InvitationAcceptSearch {
|
|
8
|
+
const token = typeof input.token === 'string' && input.token.length > 0 ? input.token : undefined;
|
|
9
|
+
return token ? {token} : {};
|
|
10
|
+
}
|