@shipfox/client-invitations 4.0.0 → 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 +12 -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-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/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 +2 -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.js +13 -13
- package/dist/pages/invitation-accept-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -4
- package/src/complete-acceptance.ts +1 -1
- package/src/core/invitation-preview.ts +18 -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 +2 -0
- package/src/pages/invitation-accept-page.tsx +15 -15
- 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": "5.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -31,11 +31,10 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@swc/helpers": "^0.5.17",
|
|
33
33
|
"@tanstack/react-query": "^5.101.0",
|
|
34
|
+
"@shipfox/api-auth-dto": "7.1.0",
|
|
34
35
|
"@shipfox/api-workspaces-dto": "6.0.0",
|
|
35
36
|
"@shipfox/client-api": "4.0.0",
|
|
36
|
-
"@shipfox/
|
|
37
|
-
"@shipfox/client-shell": "4.0.0",
|
|
38
|
-
"@shipfox/client-auth": "4.0.0",
|
|
37
|
+
"@shipfox/client-shell": "5.0.0",
|
|
39
38
|
"@shipfox/client-ui": "4.0.0",
|
|
40
39
|
"@shipfox/react-ui": "0.3.5"
|
|
41
40
|
},
|
|
@@ -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
|
+
}
|
|
@@ -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,9 @@
|
|
|
1
|
+
export {type InvitationPreview, pendingInvitation} from '#core/invitation-preview.js';
|
|
1
2
|
export {completeInvitationAcceptance} from './complete-acceptance.js';
|
|
2
3
|
export {useAcceptInvitation} from './hooks/api/accept-invitation.js';
|
|
3
4
|
export {
|
|
4
5
|
previewInvitationQueryKey,
|
|
6
|
+
previewInvitationQueryOptions,
|
|
5
7
|
usePreviewInvitation,
|
|
6
8
|
} from './hooks/api/preview-invitation.js';
|
|
7
9
|
export {InvitationAcceptPage} from './pages/invitation-accept-page.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {AuthShell, useAuthState, useRefreshAuth} from '@shipfox/client-
|
|
1
|
+
import {AuthShell, useAuthState, useRefreshAuth} from '@shipfox/client-shell/runtime';
|
|
2
2
|
import {createSingleFlight} from '@shipfox/client-ui';
|
|
3
3
|
import {Button} from '@shipfox/react-ui/button';
|
|
4
4
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
@@ -74,7 +74,7 @@ export function InvitationAcceptPage() {
|
|
|
74
74
|
|
|
75
75
|
hasKickedAccept.current = true;
|
|
76
76
|
invitationAccepts
|
|
77
|
-
.run(token, async () => await runAccept({token, workspaceName: data.
|
|
77
|
+
.run(token, async () => await runAccept({token, workspaceName: data.workspaceName}))
|
|
78
78
|
.catch(() => {
|
|
79
79
|
// The mutation surfaces its error via accept.error; the UI re-renders
|
|
80
80
|
// into the auth-match-error state below.
|
|
@@ -137,7 +137,7 @@ export function InvitationAcceptPage() {
|
|
|
137
137
|
return (
|
|
138
138
|
<AuthShell title="Invitation expired" description="The link is no longer accepting joins.">
|
|
139
139
|
<Callout role="alert" type="error">
|
|
140
|
-
This invitation expired on {formatDate(data.
|
|
140
|
+
This invitation expired on {formatDate(data.expiresAt)}. Ask your administrator to send a
|
|
141
141
|
new one.
|
|
142
142
|
</Callout>
|
|
143
143
|
<GoHomeButton navigate={navigate} />
|
|
@@ -150,12 +150,12 @@ export function InvitationAcceptPage() {
|
|
|
150
150
|
toast.info(`This invitation has already been accepted.`),
|
|
151
151
|
);
|
|
152
152
|
return (
|
|
153
|
-
<AuthShell title="Invitation already used" description={data.
|
|
153
|
+
<AuthShell title="Invitation already used" description={data.workspaceName}>
|
|
154
154
|
<Callout role="alert" type="info">
|
|
155
155
|
This invitation has already been accepted.{' '}
|
|
156
156
|
{auth.isAuthenticated
|
|
157
|
-
? `Open your dashboard to access ${data.
|
|
158
|
-
: `Log in to access ${data.
|
|
157
|
+
? `Open your dashboard to access ${data.workspaceName} if your account is a member.`
|
|
158
|
+
: `Log in to access ${data.workspaceName}.`}
|
|
159
159
|
</Callout>
|
|
160
160
|
{auth.isAuthenticated ? null : (
|
|
161
161
|
<Button asChild className="w-full">
|
|
@@ -168,8 +168,8 @@ export function InvitationAcceptPage() {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
const inviterLine =
|
|
171
|
-
data.
|
|
172
|
-
? `Invited by ${data.
|
|
171
|
+
data.invitedByDisplay != null
|
|
172
|
+
? `Invited by ${data.invitedByDisplay} to join as ${data.email}.`
|
|
173
173
|
: `Invited to join as ${data.email}.`;
|
|
174
174
|
|
|
175
175
|
if (!auth.isAuthenticated) {
|
|
@@ -177,7 +177,7 @@ export function InvitationAcceptPage() {
|
|
|
177
177
|
const signupHref = `/auth/signup?redirect=${encodeURIComponent(redirect)}`;
|
|
178
178
|
const loginHref = `/auth/login?redirect=${encodeURIComponent(redirect)}`;
|
|
179
179
|
return (
|
|
180
|
-
<AuthShell title={data.
|
|
180
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
181
181
|
<div className="flex flex-col gap-16">
|
|
182
182
|
<Button asChild className="w-full">
|
|
183
183
|
<Link to={signupHref}>Create account</Link>
|
|
@@ -199,7 +199,7 @@ export function InvitationAcceptPage() {
|
|
|
199
199
|
const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;
|
|
200
200
|
const logoutHref = `/auth/logout?redirect=${encodeURIComponent(redirect)}`;
|
|
201
201
|
return (
|
|
202
|
-
<AuthShell title={data.
|
|
202
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
203
203
|
<Callout role="alert" type="warning">
|
|
204
204
|
You're signed in as {auth.user?.email}, but this invitation is for {data.email}.
|
|
205
205
|
</Callout>
|
|
@@ -223,15 +223,15 @@ export function InvitationAcceptPage() {
|
|
|
223
223
|
// result. Show either the pending state or the error state.
|
|
224
224
|
if (accept.isError) {
|
|
225
225
|
return (
|
|
226
|
-
<AuthShell title={data.
|
|
226
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
227
227
|
<Callout role="alert" type="error">
|
|
228
|
-
We couldn't add you to {data.
|
|
228
|
+
We couldn't add you to {data.workspaceName}. Try again in a moment.
|
|
229
229
|
</Callout>
|
|
230
230
|
<Button
|
|
231
231
|
className="w-full"
|
|
232
232
|
isLoading={accept.isPending}
|
|
233
233
|
onClick={() => {
|
|
234
|
-
runAccept({token, workspaceName: data.
|
|
234
|
+
runAccept({token, workspaceName: data.workspaceName}).catch(() => {
|
|
235
235
|
// The mutation state drives the rendered error; avoid an
|
|
236
236
|
// unhandled rejection from the click handler.
|
|
237
237
|
});
|
|
@@ -244,11 +244,11 @@ export function InvitationAcceptPage() {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
return (
|
|
247
|
-
<AuthShell title={data.
|
|
247
|
+
<AuthShell title={data.workspaceName} description={inviterLine}>
|
|
248
248
|
<div className="flex flex-col items-center gap-12" aria-live="polite">
|
|
249
249
|
<ShipfoxLoader />
|
|
250
250
|
<Text size="sm" className="text-foreground-neutral-subtle">
|
|
251
|
-
Adding you to {data.
|
|
251
|
+
Adding you to {data.workspaceName}…
|
|
252
252
|
</Text>
|
|
253
253
|
</div>
|
|
254
254
|
</AuthShell>
|