@shipfox/client-invitations 0.2.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 (49) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +111 -0
  5. package/LICENSE +21 -0
  6. package/dist/complete-acceptance.d.ts +17 -0
  7. package/dist/complete-acceptance.d.ts.map +1 -0
  8. package/dist/complete-acceptance.js +29 -0
  9. package/dist/complete-acceptance.js.map +1 -0
  10. package/dist/feature.d.ts +9 -0
  11. package/dist/feature.d.ts.map +1 -0
  12. package/dist/feature.js +13 -0
  13. package/dist/feature.js.map +1 -0
  14. package/dist/hooks/api/accept-invitation.d.ts +11 -0
  15. package/dist/hooks/api/accept-invitation.d.ts.map +1 -0
  16. package/dist/hooks/api/accept-invitation.js +15 -0
  17. package/dist/hooks/api/accept-invitation.js.map +1 -0
  18. package/dist/hooks/api/preview-invitation.d.ts +19 -0
  19. package/dist/hooks/api/preview-invitation.d.ts.map +1 -0
  20. package/dist/hooks/api/preview-invitation.js +24 -0
  21. package/dist/hooks/api/preview-invitation.js.map +1 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/pages/invitation-accept-page.d.ts +2 -0
  27. package/dist/pages/invitation-accept-page.d.ts.map +1 -0
  28. package/dist/pages/invitation-accept-page.js +384 -0
  29. package/dist/pages/invitation-accept-page.js.map +1 -0
  30. package/dist/routes/accept.d.ts +6 -0
  31. package/dist/routes/accept.d.ts.map +1 -0
  32. package/dist/routes/accept.js +7 -0
  33. package/dist/routes/accept.js.map +1 -0
  34. package/dist/tsconfig.test.tsbuildinfo +1 -0
  35. package/package.json +98 -0
  36. package/src/complete-acceptance.test.ts +55 -0
  37. package/src/complete-acceptance.ts +33 -0
  38. package/src/feature.ts +12 -0
  39. package/src/hooks/api/accept-invitation.ts +17 -0
  40. package/src/hooks/api/preview-invitation.ts +23 -0
  41. package/src/index.ts +7 -0
  42. package/src/pages/invitation-accept-page.tsx +298 -0
  43. package/src/routes/accept.tsx +4 -0
  44. package/test/setup.ts +3 -0
  45. package/tsconfig.build.json +9 -0
  46. package/tsconfig.build.tsbuildinfo +1 -0
  47. package/tsconfig.json +3 -0
  48. package/tsconfig.test.json +8 -0
  49. package/vitest.config.ts +31 -0
@@ -0,0 +1,384 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { AuthShell, useAuthState, useRefreshAuth } from '@shipfox/client-auth';
3
+ import { Button } from '@shipfox/react-ui/button';
4
+ import { Callout } from '@shipfox/react-ui/callout';
5
+ import { ShipfoxLoader } from '@shipfox/react-ui/loader';
6
+ import { toast } from '@shipfox/react-ui/toast';
7
+ import { Text } from '@shipfox/react-ui/typography';
8
+ import { formatDate } from '@shipfox/react-ui/utils';
9
+ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
10
+ import { useCallback, useEffect, useRef } from 'react';
11
+ import { completeInvitationAcceptance } from '#complete-acceptance.js';
12
+ import { useAcceptInvitation } from '#hooks/api/accept-invitation.js';
13
+ import { usePreviewInvitation } from '#hooks/api/preview-invitation.js';
14
+ // Module-scoped guard so React StrictMode's double-mount in dev does not
15
+ // fire the accept mutation twice for the same token.
16
+ const inFlightAccepts = new Set();
17
+ const toastedTerminals = new Set();
18
+ const TOASTED_TERMINALS_MAX = 32;
19
+ export function InvitationAcceptPage() {
20
+ const search = useSearch({
21
+ strict: false
22
+ });
23
+ const token = typeof search.token === 'string' && search.token.length > 0 ? search.token : undefined;
24
+ const navigate = useNavigate();
25
+ const auth = useAuthState();
26
+ const refreshAuth = useRefreshAuth();
27
+ const preview = usePreviewInvitation(token);
28
+ const accept = useAcceptInvitation();
29
+ const hasKickedAccept = useRef(false);
30
+ useEffect(()=>{
31
+ if (!token) {
32
+ toast.error('This invitation link is missing a token.');
33
+ const timeout = window.setTimeout(()=>{
34
+ navigate({
35
+ to: '/',
36
+ replace: true
37
+ });
38
+ }, 2000);
39
+ return ()=>window.clearTimeout(timeout);
40
+ }
41
+ return undefined;
42
+ }, [
43
+ navigate,
44
+ token
45
+ ]);
46
+ const completeAccept = useCallback(async (workspaceId, workspaceName)=>{
47
+ await completeInvitationAcceptance({
48
+ workspaceId,
49
+ workspaceName,
50
+ refreshAuth,
51
+ navigate
52
+ });
53
+ }, [
54
+ navigate,
55
+ refreshAuth
56
+ ]);
57
+ const runAccept = useCallback(async (params)=>{
58
+ const result = await accept.mutateAsync({
59
+ token: params.token
60
+ });
61
+ await completeAccept(result.membership.workspace_id, params.workspaceName);
62
+ }, [
63
+ accept,
64
+ completeAccept
65
+ ]);
66
+ // Auto-accept when authenticated and emails match (also handles already_used
67
+ // for the canonical authenticated path: backend returns already_member=true).
68
+ useEffect(()=>{
69
+ if (!token) return;
70
+ if (auth.isLoading) return;
71
+ if (hasKickedAccept.current) return;
72
+ if (inFlightAccepts.has(token)) return;
73
+ const data = preview.data;
74
+ if (!data) return;
75
+ if (data.status !== 'pending') return;
76
+ if (!auth.isAuthenticated) return;
77
+ const authedEmail = auth.user?.email?.toLowerCase();
78
+ if (authedEmail !== data.email.toLowerCase()) return;
79
+ hasKickedAccept.current = true;
80
+ inFlightAccepts.add(token);
81
+ runAccept({
82
+ token,
83
+ workspaceName: data.workspace_name
84
+ }).catch(()=>{
85
+ // The mutation surfaces its error via accept.error; the UI re-renders
86
+ // into the auth-match-error state below.
87
+ }).finally(()=>{
88
+ inFlightAccepts.delete(token);
89
+ });
90
+ }, [
91
+ auth.isAuthenticated,
92
+ auth.isLoading,
93
+ auth.user?.email,
94
+ preview.data,
95
+ runAccept,
96
+ token
97
+ ]);
98
+ if (!token) {
99
+ return /*#__PURE__*/ _jsxs(AuthShell, {
100
+ title: "Invalid link",
101
+ description: "This invitation link is incomplete.",
102
+ children: [
103
+ /*#__PURE__*/ _jsx(Callout, {
104
+ role: "alert",
105
+ type: "error",
106
+ children: "This invitation link is missing a token. Ask your administrator to resend it."
107
+ }),
108
+ /*#__PURE__*/ _jsx(Text, {
109
+ size: "sm",
110
+ className: "text-center text-foreground-neutral-muted",
111
+ "aria-live": "polite",
112
+ children: "Redirecting to the dashboard…"
113
+ })
114
+ ]
115
+ });
116
+ }
117
+ if (preview.isLoading) {
118
+ return /*#__PURE__*/ _jsx(AuthShell, {
119
+ title: "Loading invitation",
120
+ description: "One moment please.",
121
+ children: /*#__PURE__*/ _jsx(CenteredLoader, {})
122
+ });
123
+ }
124
+ if (preview.isError) {
125
+ return /*#__PURE__*/ _jsxs(AuthShell, {
126
+ title: "Couldn't load invitation",
127
+ description: "Try again in a moment.",
128
+ children: [
129
+ /*#__PURE__*/ _jsx(Callout, {
130
+ role: "alert",
131
+ type: "error",
132
+ children: "We couldn't reach the server. Check your connection and retry."
133
+ }),
134
+ /*#__PURE__*/ _jsx(Button, {
135
+ className: "w-full",
136
+ onClick: ()=>preview.refetch(),
137
+ children: "Try again"
138
+ })
139
+ ]
140
+ });
141
+ }
142
+ const data = preview.data;
143
+ if (!data) {
144
+ return null;
145
+ }
146
+ if (data.status === 'invalid') {
147
+ notifyOnce(`invalid:${token}`, ()=>toast.error('This invitation link is no longer valid.'));
148
+ return /*#__PURE__*/ _jsxs(AuthShell, {
149
+ title: "Invalid invitation",
150
+ description: "The link you used didn't work.",
151
+ children: [
152
+ /*#__PURE__*/ _jsx(Callout, {
153
+ role: "alert",
154
+ type: "error",
155
+ children: "This invitation link is not valid. Ask your administrator to send a new one."
156
+ }),
157
+ /*#__PURE__*/ _jsx(GoHomeButton, {
158
+ navigate: navigate
159
+ })
160
+ ]
161
+ });
162
+ }
163
+ if (data.status === 'expired') {
164
+ notifyOnce(`expired:${token}`, ()=>toast.error('This invitation has expired.'));
165
+ return /*#__PURE__*/ _jsxs(AuthShell, {
166
+ title: "Invitation expired",
167
+ description: "The link is no longer accepting joins.",
168
+ children: [
169
+ /*#__PURE__*/ _jsxs(Callout, {
170
+ role: "alert",
171
+ type: "error",
172
+ children: [
173
+ "This invitation expired on ",
174
+ formatDate(data.expires_at),
175
+ ". Ask your administrator to send a new one."
176
+ ]
177
+ }),
178
+ /*#__PURE__*/ _jsx(GoHomeButton, {
179
+ navigate: navigate
180
+ })
181
+ ]
182
+ });
183
+ }
184
+ if (data.status === 'already_used') {
185
+ notifyOnce(`already-used:${token}`, ()=>toast.info(`This invitation has already been accepted.`));
186
+ return /*#__PURE__*/ _jsxs(AuthShell, {
187
+ title: "Invitation already used",
188
+ description: data.workspace_name,
189
+ children: [
190
+ /*#__PURE__*/ _jsxs(Callout, {
191
+ role: "alert",
192
+ type: "info",
193
+ children: [
194
+ "This invitation has already been accepted.",
195
+ ' ',
196
+ auth.isAuthenticated ? `Open your dashboard to access ${data.workspace_name} if your account is a member.` : `Log in to access ${data.workspace_name}.`
197
+ ]
198
+ }),
199
+ auth.isAuthenticated ? null : /*#__PURE__*/ _jsx(Button, {
200
+ asChild: true,
201
+ className: "w-full",
202
+ children: /*#__PURE__*/ _jsx(Link, {
203
+ to: "/auth/login",
204
+ children: "Log in"
205
+ })
206
+ }),
207
+ /*#__PURE__*/ _jsx(GoHomeButton, {
208
+ navigate: navigate,
209
+ secondary: true
210
+ })
211
+ ]
212
+ });
213
+ }
214
+ const inviterLine = data.invited_by_display != null ? `Invited by ${data.invited_by_display} to join as ${data.email}.` : `Invited to join as ${data.email}.`;
215
+ if (!auth.isAuthenticated) {
216
+ const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;
217
+ const signupHref = `/auth/signup?redirect=${encodeURIComponent(redirect)}`;
218
+ const loginHref = `/auth/login?redirect=${encodeURIComponent(redirect)}`;
219
+ return /*#__PURE__*/ _jsxs(AuthShell, {
220
+ title: data.workspace_name,
221
+ description: inviterLine,
222
+ children: [
223
+ /*#__PURE__*/ _jsxs("div", {
224
+ className: "flex flex-col gap-16",
225
+ children: [
226
+ /*#__PURE__*/ _jsx(Button, {
227
+ asChild: true,
228
+ className: "w-full",
229
+ children: /*#__PURE__*/ _jsx(Link, {
230
+ to: signupHref,
231
+ children: "Create account"
232
+ })
233
+ }),
234
+ /*#__PURE__*/ _jsx(Button, {
235
+ asChild: true,
236
+ className: "w-full",
237
+ variant: "secondary",
238
+ children: /*#__PURE__*/ _jsx(Link, {
239
+ to: loginHref,
240
+ children: "I already have an account"
241
+ })
242
+ })
243
+ ]
244
+ }),
245
+ /*#__PURE__*/ _jsxs(Text, {
246
+ size: "xs",
247
+ className: "text-center text-foreground-neutral-muted",
248
+ children: [
249
+ "Not ",
250
+ data.email,
251
+ "? Contact your administrator."
252
+ ]
253
+ })
254
+ ]
255
+ });
256
+ }
257
+ const viewerEmail = auth.user?.email?.toLowerCase();
258
+ const inviteeEmail = data.email.toLowerCase();
259
+ if (viewerEmail !== inviteeEmail) {
260
+ const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;
261
+ const logoutHref = `/auth/logout?redirect=${encodeURIComponent(redirect)}`;
262
+ return /*#__PURE__*/ _jsxs(AuthShell, {
263
+ title: data.workspace_name,
264
+ description: inviterLine,
265
+ children: [
266
+ /*#__PURE__*/ _jsxs(Callout, {
267
+ role: "alert",
268
+ type: "warning",
269
+ children: [
270
+ "You're signed in as ",
271
+ auth.user?.email,
272
+ ", but this invitation is for ",
273
+ data.email,
274
+ "."
275
+ ]
276
+ }),
277
+ /*#__PURE__*/ _jsxs("div", {
278
+ className: "flex flex-col gap-16",
279
+ children: [
280
+ /*#__PURE__*/ _jsx(Button, {
281
+ asChild: true,
282
+ className: "w-full",
283
+ children: /*#__PURE__*/ _jsx(Link, {
284
+ to: logoutHref,
285
+ children: "Log out and continue"
286
+ })
287
+ }),
288
+ /*#__PURE__*/ _jsx(Button, {
289
+ className: "w-full",
290
+ variant: "secondary",
291
+ onClick: ()=>navigate({
292
+ to: '/',
293
+ replace: true
294
+ }),
295
+ children: "Cancel"
296
+ })
297
+ ]
298
+ })
299
+ ]
300
+ });
301
+ }
302
+ // Authenticated + matches — auto-accept is in flight or about to render its
303
+ // result. Show either the pending state or the error state.
304
+ if (accept.isError) {
305
+ return /*#__PURE__*/ _jsxs(AuthShell, {
306
+ title: data.workspace_name,
307
+ description: inviterLine,
308
+ children: [
309
+ /*#__PURE__*/ _jsxs(Callout, {
310
+ role: "alert",
311
+ type: "error",
312
+ children: [
313
+ "We couldn't add you to ",
314
+ data.workspace_name,
315
+ ". Try again in a moment."
316
+ ]
317
+ }),
318
+ /*#__PURE__*/ _jsx(Button, {
319
+ className: "w-full",
320
+ isLoading: accept.isPending,
321
+ onClick: ()=>{
322
+ runAccept({
323
+ token,
324
+ workspaceName: data.workspace_name
325
+ }).catch(()=>{
326
+ // The mutation state drives the rendered error; avoid an
327
+ // unhandled rejection from the click handler.
328
+ });
329
+ },
330
+ children: "Try again"
331
+ })
332
+ ]
333
+ });
334
+ }
335
+ return /*#__PURE__*/ _jsx(AuthShell, {
336
+ title: data.workspace_name,
337
+ description: inviterLine,
338
+ children: /*#__PURE__*/ _jsxs("div", {
339
+ className: "flex flex-col items-center gap-12",
340
+ "aria-live": "polite",
341
+ children: [
342
+ /*#__PURE__*/ _jsx(ShipfoxLoader, {}),
343
+ /*#__PURE__*/ _jsxs(Text, {
344
+ size: "sm",
345
+ className: "text-foreground-neutral-subtle",
346
+ children: [
347
+ "Adding you to ",
348
+ data.workspace_name,
349
+ "…"
350
+ ]
351
+ })
352
+ ]
353
+ })
354
+ });
355
+ }
356
+ function CenteredLoader() {
357
+ return /*#__PURE__*/ _jsx("div", {
358
+ className: "flex justify-center py-16",
359
+ "aria-busy": "true",
360
+ children: /*#__PURE__*/ _jsx(ShipfoxLoader, {})
361
+ });
362
+ }
363
+ function GoHomeButton({ navigate, secondary }) {
364
+ return /*#__PURE__*/ _jsx(Button, {
365
+ className: "w-full",
366
+ variant: secondary ? 'secondary' : 'primary',
367
+ onClick: ()=>navigate({
368
+ to: '/',
369
+ replace: true
370
+ }),
371
+ children: "Go to dashboard"
372
+ });
373
+ }
374
+ function notifyOnce(key, fn) {
375
+ if (toastedTerminals.has(key)) return;
376
+ if (toastedTerminals.size >= TOASTED_TERMINALS_MAX) {
377
+ const oldest = toastedTerminals.values().next().value;
378
+ if (oldest !== undefined) toastedTerminals.delete(oldest);
379
+ }
380
+ toastedTerminals.add(key);
381
+ fn();
382
+ }
383
+
384
+ //# sourceMappingURL=invitation-accept-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/invitation-accept-page.tsx"],"sourcesContent":["import {AuthShell, useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {ShipfoxLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {formatDate} from '@shipfox/react-ui/utils';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useCallback, useEffect, useRef} from 'react';\nimport {completeInvitationAcceptance} from '#complete-acceptance.js';\nimport {useAcceptInvitation} from '#hooks/api/accept-invitation.js';\nimport {usePreviewInvitation} from '#hooks/api/preview-invitation.js';\n\n// Module-scoped guard so React StrictMode's double-mount in dev does not\n// fire the accept mutation twice for the same token.\nconst inFlightAccepts = new Set<string>();\nconst toastedTerminals = new Set<string>();\nconst TOASTED_TERMINALS_MAX = 32;\n\nexport function InvitationAcceptPage() {\n const search = useSearch({strict: false}) as {token?: unknown};\n const token =\n typeof search.token === 'string' && search.token.length > 0 ? search.token : undefined;\n const navigate = useNavigate();\n const auth = useAuthState();\n const refreshAuth = useRefreshAuth();\n const preview = usePreviewInvitation(token);\n const accept = useAcceptInvitation();\n const hasKickedAccept = useRef(false);\n\n useEffect(() => {\n if (!token) {\n toast.error('This invitation link is missing a token.');\n const timeout = window.setTimeout(() => {\n navigate({to: '/', replace: true});\n }, 2000);\n return () => window.clearTimeout(timeout);\n }\n return undefined;\n }, [navigate, token]);\n\n const completeAccept = useCallback(\n async (workspaceId: string, workspaceName: string) => {\n await completeInvitationAcceptance({\n workspaceId,\n workspaceName,\n refreshAuth,\n navigate,\n });\n },\n [navigate, refreshAuth],\n );\n\n const runAccept = useCallback(\n async (params: {token: string; workspaceName: string}) => {\n const result = await accept.mutateAsync({token: params.token});\n await completeAccept(result.membership.workspace_id, params.workspaceName);\n },\n [accept, completeAccept],\n );\n\n // Auto-accept when authenticated and emails match (also handles already_used\n // for the canonical authenticated path: backend returns already_member=true).\n useEffect(() => {\n if (!token) return;\n if (auth.isLoading) return;\n if (hasKickedAccept.current) return;\n if (inFlightAccepts.has(token)) return;\n const data = preview.data;\n if (!data) return;\n if (data.status !== 'pending') return;\n if (!auth.isAuthenticated) return;\n\n const authedEmail = auth.user?.email?.toLowerCase();\n if (authedEmail !== data.email.toLowerCase()) return;\n\n hasKickedAccept.current = true;\n inFlightAccepts.add(token);\n\n runAccept({token, workspaceName: data.workspace_name})\n .catch(() => {\n // The mutation surfaces its error via accept.error; the UI re-renders\n // into the auth-match-error state below.\n })\n .finally(() => {\n inFlightAccepts.delete(token);\n });\n }, [auth.isAuthenticated, auth.isLoading, auth.user?.email, preview.data, runAccept, token]);\n\n if (!token) {\n return (\n <AuthShell title=\"Invalid link\" description=\"This invitation link is incomplete.\">\n <Callout role=\"alert\" type=\"error\">\n This invitation link is missing a token. Ask your administrator to resend it.\n </Callout>\n <Text size=\"sm\" className=\"text-center text-foreground-neutral-muted\" aria-live=\"polite\">\n Redirecting to the dashboard…\n </Text>\n </AuthShell>\n );\n }\n\n if (preview.isLoading) {\n return (\n <AuthShell title=\"Loading invitation\" description=\"One moment please.\">\n <CenteredLoader />\n </AuthShell>\n );\n }\n\n if (preview.isError) {\n return (\n <AuthShell title=\"Couldn't load invitation\" description=\"Try again in a moment.\">\n <Callout role=\"alert\" type=\"error\">\n We couldn't reach the server. Check your connection and retry.\n </Callout>\n <Button className=\"w-full\" onClick={() => preview.refetch()}>\n Try again\n </Button>\n </AuthShell>\n );\n }\n\n const data = preview.data;\n if (!data) {\n return null;\n }\n\n if (data.status === 'invalid') {\n notifyOnce(`invalid:${token}`, () => toast.error('This invitation link is no longer valid.'));\n return (\n <AuthShell title=\"Invalid invitation\" description=\"The link you used didn't work.\">\n <Callout role=\"alert\" type=\"error\">\n This invitation link is not valid. Ask your administrator to send a new one.\n </Callout>\n <GoHomeButton navigate={navigate} />\n </AuthShell>\n );\n }\n\n if (data.status === 'expired') {\n notifyOnce(`expired:${token}`, () => toast.error('This invitation has expired.'));\n return (\n <AuthShell title=\"Invitation expired\" description=\"The link is no longer accepting joins.\">\n <Callout role=\"alert\" type=\"error\">\n This invitation expired on {formatDate(data.expires_at)}. Ask your administrator to send a\n new one.\n </Callout>\n <GoHomeButton navigate={navigate} />\n </AuthShell>\n );\n }\n\n if (data.status === 'already_used') {\n notifyOnce(`already-used:${token}`, () =>\n toast.info(`This invitation has already been accepted.`),\n );\n return (\n <AuthShell title=\"Invitation already used\" description={data.workspace_name}>\n <Callout role=\"alert\" type=\"info\">\n This invitation has already been accepted.{' '}\n {auth.isAuthenticated\n ? `Open your dashboard to access ${data.workspace_name} if your account is a member.`\n : `Log in to access ${data.workspace_name}.`}\n </Callout>\n {auth.isAuthenticated ? null : (\n <Button asChild className=\"w-full\">\n <Link to=\"/auth/login\">Log in</Link>\n </Button>\n )}\n <GoHomeButton navigate={navigate} secondary />\n </AuthShell>\n );\n }\n\n const inviterLine =\n data.invited_by_display != null\n ? `Invited by ${data.invited_by_display} to join as ${data.email}.`\n : `Invited to join as ${data.email}.`;\n\n if (!auth.isAuthenticated) {\n const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;\n const signupHref = `/auth/signup?redirect=${encodeURIComponent(redirect)}`;\n const loginHref = `/auth/login?redirect=${encodeURIComponent(redirect)}`;\n return (\n <AuthShell title={data.workspace_name} description={inviterLine}>\n <div className=\"flex flex-col gap-16\">\n <Button asChild className=\"w-full\">\n <Link to={signupHref}>Create account</Link>\n </Button>\n <Button asChild className=\"w-full\" variant=\"secondary\">\n <Link to={loginHref}>I already have an account</Link>\n </Button>\n </div>\n <Text size=\"xs\" className=\"text-center text-foreground-neutral-muted\">\n Not {data.email}? Contact your administrator.\n </Text>\n </AuthShell>\n );\n }\n\n const viewerEmail = auth.user?.email?.toLowerCase();\n const inviteeEmail = data.email.toLowerCase();\n if (viewerEmail !== inviteeEmail) {\n const redirect = `/invitations/accept?token=${encodeURIComponent(token)}`;\n const logoutHref = `/auth/logout?redirect=${encodeURIComponent(redirect)}`;\n return (\n <AuthShell title={data.workspace_name} description={inviterLine}>\n <Callout role=\"alert\" type=\"warning\">\n You're signed in as {auth.user?.email}, but this invitation is for {data.email}.\n </Callout>\n <div className=\"flex flex-col gap-16\">\n <Button asChild className=\"w-full\">\n <Link to={logoutHref}>Log out and continue</Link>\n </Button>\n <Button\n className=\"w-full\"\n variant=\"secondary\"\n onClick={() => navigate({to: '/', replace: true})}\n >\n Cancel\n </Button>\n </div>\n </AuthShell>\n );\n }\n\n // Authenticated + matches — auto-accept is in flight or about to render its\n // result. Show either the pending state or the error state.\n if (accept.isError) {\n return (\n <AuthShell title={data.workspace_name} description={inviterLine}>\n <Callout role=\"alert\" type=\"error\">\n We couldn't add you to {data.workspace_name}. Try again in a moment.\n </Callout>\n <Button\n className=\"w-full\"\n isLoading={accept.isPending}\n onClick={() => {\n runAccept({token, workspaceName: data.workspace_name}).catch(() => {\n // The mutation state drives the rendered error; avoid an\n // unhandled rejection from the click handler.\n });\n }}\n >\n Try again\n </Button>\n </AuthShell>\n );\n }\n\n return (\n <AuthShell title={data.workspace_name} description={inviterLine}>\n <div className=\"flex flex-col items-center gap-12\" aria-live=\"polite\">\n <ShipfoxLoader />\n <Text size=\"sm\" className=\"text-foreground-neutral-subtle\">\n Adding you to {data.workspace_name}…\n </Text>\n </div>\n </AuthShell>\n );\n}\n\nfunction CenteredLoader() {\n return (\n <div className=\"flex justify-center py-16\" aria-busy=\"true\">\n <ShipfoxLoader />\n </div>\n );\n}\n\nfunction GoHomeButton({\n navigate,\n secondary,\n}: {\n navigate: ReturnType<typeof useNavigate>;\n secondary?: boolean;\n}) {\n return (\n <Button\n className=\"w-full\"\n variant={secondary ? 'secondary' : 'primary'}\n onClick={() => navigate({to: '/', replace: true})}\n >\n Go to dashboard\n </Button>\n );\n}\n\nfunction notifyOnce(key: string, fn: () => void): void {\n if (toastedTerminals.has(key)) return;\n if (toastedTerminals.size >= TOASTED_TERMINALS_MAX) {\n const oldest = toastedTerminals.values().next().value;\n if (oldest !== undefined) toastedTerminals.delete(oldest);\n }\n toastedTerminals.add(key);\n fn();\n}\n"],"names":["AuthShell","useAuthState","useRefreshAuth","Button","Callout","ShipfoxLoader","toast","Text","formatDate","Link","useNavigate","useSearch","useCallback","useEffect","useRef","completeInvitationAcceptance","useAcceptInvitation","usePreviewInvitation","inFlightAccepts","Set","toastedTerminals","TOASTED_TERMINALS_MAX","InvitationAcceptPage","search","strict","token","length","undefined","navigate","auth","refreshAuth","preview","accept","hasKickedAccept","error","timeout","window","setTimeout","to","replace","clearTimeout","completeAccept","workspaceId","workspaceName","runAccept","params","result","mutateAsync","membership","workspace_id","isLoading","current","has","data","status","isAuthenticated","authedEmail","user","email","toLowerCase","add","workspace_name","catch","finally","delete","title","description","role","type","size","className","aria-live","CenteredLoader","isError","onClick","refetch","notifyOnce","GoHomeButton","expires_at","info","asChild","secondary","inviterLine","invited_by_display","redirect","encodeURIComponent","signupHref","loginHref","div","variant","viewerEmail","inviteeEmail","logoutHref","isPending","aria-busy","key","fn","oldest","values","next","value"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { InvitationAcceptPage } from '#pages/invitation-accept-page.js';
2
+ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
3
+ readonly component: typeof InvitationAcceptPage;
4
+ }>;
5
+ export default _default;
6
+ //# sourceMappingURL=accept.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accept.d.ts","sourceRoot":"","sources":["../../src/routes/accept.tsx"],"names":[],"mappings":"AACA,OAAO,EAAC,oBAAoB,EAAC,MAAM,kCAAkC,CAAC;;;;AAEtE,wBAA8D"}
@@ -0,0 +1,7 @@
1
+ import { defineRoute } from '@shipfox/client-shell/runtime';
2
+ import { InvitationAcceptPage } from '#pages/invitation-accept-page.js';
3
+ export default defineRoute({
4
+ component: InvitationAcceptPage
5
+ });
6
+
7
+ //# sourceMappingURL=accept.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/accept.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {InvitationAcceptPage} from '#pages/invitation-accept-page.js';\n\nexport default defineRoute({component: InvitationAcceptPage});\n"],"names":["defineRoute","InvitationAcceptPage","component"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,oBAAoB,QAAO,mCAAmC;AAEtE,eAAeD,YAAY;IAACE,WAAWD;AAAoB,GAAG"}