@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.
Files changed (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +12 -0
  3. package/dist/complete-acceptance.d.ts +1 -1
  4. package/dist/complete-acceptance.js +1 -1
  5. package/dist/complete-acceptance.js.map +1 -1
  6. package/dist/core/invitation-preview.d.ts +21 -0
  7. package/dist/core/invitation-preview.d.ts.map +1 -0
  8. package/dist/core/invitation-preview.js +5 -0
  9. package/dist/core/invitation-preview.js.map +1 -0
  10. package/dist/hooks/api/invitation-preview-mapper.d.ts +4 -0
  11. package/dist/hooks/api/invitation-preview-mapper.d.ts.map +1 -0
  12. package/dist/hooks/api/invitation-preview-mapper.js +32 -0
  13. package/dist/hooks/api/invitation-preview-mapper.js.map +1 -0
  14. package/dist/hooks/api/preview-invitation.d.ts +12 -7
  15. package/dist/hooks/api/preview-invitation.d.ts.map +1 -1
  16. package/dist/hooks/api/preview-invitation.js +12 -6
  17. package/dist/hooks/api/preview-invitation.js.map +1 -1
  18. package/dist/index.d.ts +2 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +2 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/pages/invitation-accept-page.js +13 -13
  23. package/dist/pages/invitation-accept-page.js.map +1 -1
  24. package/dist/tsconfig.test.tsbuildinfo +1 -1
  25. package/package.json +3 -4
  26. package/src/complete-acceptance.ts +1 -1
  27. package/src/core/invitation-preview.ts +18 -0
  28. package/src/hooks/api/invitation-preview-mapper.test.ts +36 -0
  29. package/src/hooks/api/invitation-preview-mapper.ts +22 -0
  30. package/src/hooks/api/preview-invitation.ts +24 -9
  31. package/src/index.ts +2 -0
  32. package/src/pages/invitation-accept-page.tsx +15 -15
  33. 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.0.0",
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/api-auth-dto": "7.1.0",
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-auth`.
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 type {PreviewInvitationResponseDto} from '@shipfox/api-workspaces-dto';
2
- import {apiRequest} from '@shipfox/client-api';
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
- async function previewInvitation(token: string): Promise<PreviewInvitationResponseDto> {
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
- return await apiRequest<PreviewInvitationResponseDto>(
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 usePreviewInvitation(token: string | undefined) {
16
- return useQuery({
17
- queryKey: previewInvitationQueryKey(token ?? ''),
18
- queryFn: () => previewInvitation(token as string),
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-auth';
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.workspace_name}))
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.expires_at)}. Ask your administrator to send a
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.workspace_name}>
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.workspace_name} if your account is a member.`
158
- : `Log in to access ${data.workspace_name}.`}
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.invited_by_display != null
172
- ? `Invited by ${data.invited_by_display} to join as ${data.email}.`
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.workspace_name} description={inviterLine}>
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.workspace_name} description={inviterLine}>
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.workspace_name} description={inviterLine}>
226
+ <AuthShell title={data.workspaceName} description={inviterLine}>
227
227
  <Callout role="alert" type="error">
228
- We couldn't add you to {data.workspace_name}. Try again in a moment.
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.workspace_name}).catch(() => {
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.workspace_name} description={inviterLine}>
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.workspace_name}…
251
+ Adding you to {data.workspaceName}…
252
252
  </Text>
253
253
  </div>
254
254
  </AuthShell>