@shipfox/client-integrations 2.0.0 → 4.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 +38 -0
- package/dist/pages/linear-callback-page.js +8 -12
- package/dist/pages/linear-callback-page.js.map +1 -1
- package/dist/pages/sentry-callback-page.d.ts.map +1 -1
- package/dist/pages/sentry-callback-page.js +4 -18
- package/dist/pages/sentry-callback-page.js.map +1 -1
- package/dist/pages/slack-callback-page.d.ts.map +1 -1
- package/dist/pages/slack-callback-page.js +8 -9
- package/dist/pages/slack-callback-page.js.map +1 -1
- package/dist/routes/github-callback.d.ts +1 -0
- package/dist/routes/github-callback.d.ts.map +1 -1
- package/dist/routes/github-callback.js +15 -12
- package/dist/routes/github-callback.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -39
- package/src/pages/linear-callback-page.tsx +13 -13
- package/src/pages/sentry-callback-page.tsx +16 -26
- package/src/pages/slack-callback-page.tsx +13 -10
- package/src/routes/github-callback.test.ts +17 -0
- package/src/routes/github-callback.tsx +15 -12
- package/tsconfig.build.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
$ shipfox-swc
|
|
2
|
-
Successfully compiled: 48 files with swc (
|
|
2
|
+
Successfully compiled: 48 files with swc (452.44ms)
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @shipfox/client-integrations
|
|
2
2
|
|
|
3
|
+
## 4.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2e5b718: Adds safe browser persistence and bounded callback deduplication across client flows.
|
|
8
|
+
- Updated dependencies [2e5b718]
|
|
9
|
+
- Updated dependencies [6b4a575]
|
|
10
|
+
- Updated dependencies [20e4feb]
|
|
11
|
+
- Updated dependencies [11b10f7]
|
|
12
|
+
- Updated dependencies [781a45b]
|
|
13
|
+
- @shipfox/client-ui@4.0.0
|
|
14
|
+
- @shipfox/client-shell@4.0.0
|
|
15
|
+
- @shipfox/client-api@4.0.0
|
|
16
|
+
- @shipfox/client-auth@4.0.0
|
|
17
|
+
|
|
18
|
+
## 3.0.1
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- ffc7fc9: Republishes the affected release set after recovering package publication.
|
|
23
|
+
- Updated dependencies [ffc7fc9]
|
|
24
|
+
- @shipfox/client-auth@3.0.1
|
|
25
|
+
- @shipfox/client-shell@3.0.1
|
|
26
|
+
- @shipfox/client-ui@3.0.1
|
|
27
|
+
- @shipfox/react-ui@0.3.5
|
|
28
|
+
|
|
29
|
+
## 3.0.0
|
|
30
|
+
|
|
31
|
+
### Patch Changes
|
|
32
|
+
|
|
33
|
+
- Updated dependencies [cb58afe]
|
|
34
|
+
- Updated dependencies [d735fe3]
|
|
35
|
+
- Updated dependencies [5b06cd5]
|
|
36
|
+
- @shipfox/react-ui@0.3.4
|
|
37
|
+
- @shipfox/client-shell@3.0.0
|
|
38
|
+
- @shipfox/client-auth@3.0.0
|
|
39
|
+
- @shipfox/client-ui@3.0.0
|
|
40
|
+
|
|
3
41
|
## 2.0.0
|
|
4
42
|
|
|
5
43
|
### Minor Changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
3
|
+
import { createSingleFlight } from '@shipfox/client-ui';
|
|
3
4
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
4
5
|
import { toast } from '@shipfox/react-ui/toast';
|
|
5
6
|
import { useQueryClient } from '@tanstack/react-query';
|
|
@@ -8,12 +9,11 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
8
9
|
import { CallbackStatusShell } from '#components/callback-status-shell.js';
|
|
9
10
|
import { integrationsQueryKeys, useCompleteLinearCallbackMutation } from '#hooks/api/integrations.js';
|
|
10
11
|
import { classifyLinearCallbackError, clearLinearInstallWorkspace, parseLinearCallbackQuery, readLinearInstallWorkspace, serializeLinearCallbackQuery } from '#linear-callback.js';
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const callbackRequests = new Map();
|
|
12
|
+
// Retain only recent completions: this bounds long-lived callback pages while
|
|
13
|
+
// still covering StrictMode and immediate Back/Forward remounts.
|
|
14
|
+
const callbackRequests = createSingleFlight({
|
|
15
|
+
maxTerminalResults: 32
|
|
16
|
+
});
|
|
17
17
|
// Keeps the success toast firing once per distinct callback even though the
|
|
18
18
|
// effect re-runs against the cached request as the mutation identity churns.
|
|
19
19
|
const toastedCallbacks = new Set();
|
|
@@ -40,14 +40,10 @@ export function LinearCallbackPage() {
|
|
|
40
40
|
if (!params) return;
|
|
41
41
|
let disposed = false;
|
|
42
42
|
const key = serializeLinearCallbackQuery(params);
|
|
43
|
-
|
|
44
|
-
if (!request) {
|
|
45
|
-
request = refreshAuth().then(async (session)=>await completeLinearCallback({
|
|
43
|
+
const request = callbackRequests.run(key, async ()=>await refreshAuth().then(async (session)=>await completeLinearCallback({
|
|
46
44
|
query: params,
|
|
47
45
|
token: session.token
|
|
48
|
-
}));
|
|
49
|
-
callbackRequests.set(key, request);
|
|
50
|
-
}
|
|
46
|
+
})));
|
|
51
47
|
request.then(async (connection)=>{
|
|
52
48
|
if (disposed) return;
|
|
53
49
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import type {LinearCallbackResponseDto} from '@shipfox/api-integration-linear-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, useCompleteLinearCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifyLinearCallbackError,\n clearLinearInstallWorkspace,\n type LinearCallbackFailure,\n parseLinearCallbackQuery,\n readLinearInstallWorkspace,\n serializeLinearCallbackQuery,\n} from '#linear-callback.js';\n\n//
|
|
1
|
+
{"version":3,"sources":["../../src/pages/linear-callback-page.tsx"],"sourcesContent":["import type {LinearCallbackResponseDto} from '@shipfox/api-integration-linear-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {createSingleFlight} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, useCompleteLinearCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifyLinearCallbackError,\n clearLinearInstallWorkspace,\n type LinearCallbackFailure,\n parseLinearCallbackQuery,\n readLinearInstallWorkspace,\n serializeLinearCallbackQuery,\n} from '#linear-callback.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, LinearCallbackResponseDto>({\n maxTerminalResults: 32,\n});\n// Keeps the success toast firing once per distinct callback even though the\n// effect re-runs against the cached request as the mutation identity churns.\nconst toastedCallbacks = new Set<string>();\n\nexport function LinearCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const queryClient = useQueryClient();\n const {mutateAsync: completeLinearCallback} = useCompleteLinearCallbackMutation();\n const params = useMemo(() => parseLinearCallbackQuery(search), [search]);\n const workspaceId = useMemo(() => {\n try {\n return readLinearInstallWorkspace(window.sessionStorage);\n } catch {\n return undefined;\n }\n }, []);\n const [failure, setFailure] = useState<LinearCallbackFailure | undefined>();\n useEffect(() => {\n if (!params) return;\n\n let disposed = false;\n const key = serializeLinearCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await refreshAuth().then(\n async (session) => await completeLinearCallback({query: params, token: session.token}),\n ),\n );\n\n request.then(\n async (connection) => {\n if (disposed) return;\n try {\n clearLinearInstallWorkspace(window.sessionStorage);\n } catch {\n // The successful API response remains the source of truth for navigation.\n }\n try {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n } catch {\n // Cache refresh is best effort: the successful callback is already committed server-side.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.success('Linear installed.');\n }\n try {\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: connection.workspace_id},\n replace: true,\n });\n } catch {\n // Keep the completed callback page visible if client navigation is interrupted.\n }\n },\n (error: unknown) => {\n if (disposed) return;\n setFailure(classifyLinearCallbackError(error));\n },\n );\n\n return () => {\n disposed = true;\n };\n }, [completeLinearCallback, navigate, params, queryClient, refreshAuth]);\n\n if (!params) {\n return (\n <LinearCallbackFailurePage\n failure={{\n title: 'Invalid Linear callback',\n message: 'Invalid Linear callback. Start the install again from workspace settings.',\n startOver: true,\n signIn: false,\n }}\n workspaceId={workspaceId}\n />\n );\n }\n\n if (!failure) return <FullPageLoader aria-label=\"Connecting Linear\" />;\n\n return <LinearCallbackFailurePage failure={failure} workspaceId={workspaceId} />;\n}\n\nfunction LinearCallbackFailurePage({\n failure,\n workspaceId,\n}: {\n failure: LinearCallbackFailure;\n workspaceId: string | undefined;\n}) {\n return (\n <CallbackStatusShell\n title={failure.title ?? 'Linear install could not be completed'}\n message={failure.message}\n startOver={failure.startOver}\n switchAccount={failure.signIn}\n workspaceId={workspaceId}\n installPath=\"/workspaces/$wid/integrations/linear\"\n />\n );\n}\n"],"names":["useRefreshAuth","createSingleFlight","FullPageLoader","toast","useQueryClient","useNavigate","useSearch","useEffect","useMemo","useState","CallbackStatusShell","integrationsQueryKeys","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","maxTerminalResults","toastedCallbacks","Set","LinearCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeLinearCallback","params","workspaceId","window","sessionStorage","undefined","failure","setFailure","disposed","key","request","run","then","session","query","token","connection","invalidateQueries","queryKey","connectionsByWorkspace","workspace_id","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","switchAccount","installPath"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,kBAAkB,uCAiMjC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
|
|
3
|
+
import { createSingleFlight } from '@shipfox/client-ui';
|
|
3
4
|
import { Button, ButtonLink } from '@shipfox/react-ui/button';
|
|
4
5
|
import { Callout } from '@shipfox/react-ui/callout';
|
|
5
6
|
import { Card } from '@shipfox/react-ui/card';
|
|
@@ -11,12 +12,7 @@ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
|
|
|
11
12
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
13
|
import { connectSentry, integrationsQueryKeys } from '#hooks/api/integrations.js';
|
|
13
14
|
import { classifySentryConnectError, clearSentryInstallWorkspace, parseSentryCallbackParams, preselectSentryWorkspace, readSentryInstallWorkspace } from '#sentry-callback.js';
|
|
14
|
-
|
|
15
|
-
// (double click, remount while in flight). Entries are evicted once the
|
|
16
|
-
// request settles, so Retry, and any later attempt carrying a fresh grant
|
|
17
|
-
// code, always issues a genuinely new request instead of replaying a cached
|
|
18
|
-
// outcome.
|
|
19
|
-
const connectRequests = new Map();
|
|
15
|
+
const connectRequests = createSingleFlight();
|
|
20
16
|
export function SentryCallbackPage() {
|
|
21
17
|
const search = useSearch({
|
|
22
18
|
strict: false
|
|
@@ -101,24 +97,14 @@ export function SentryCallbackPage() {
|
|
|
101
97
|
setConnectingId(workspaceId);
|
|
102
98
|
setFailure(undefined);
|
|
103
99
|
const key = `${params.installationId}|${workspaceId}|${params.code}`;
|
|
104
|
-
|
|
105
|
-
if (!request) {
|
|
106
|
-
request = refreshAuth().then((session)=>connectSentry({
|
|
100
|
+
const request = connectRequests.run(key, async ()=>await refreshAuth().then((session)=>connectSentry({
|
|
107
101
|
body: {
|
|
108
102
|
workspace_id: workspaceId,
|
|
109
103
|
code: params.code,
|
|
110
104
|
installation_id: params.installationId
|
|
111
105
|
},
|
|
112
106
|
token: session.token
|
|
113
|
-
}));
|
|
114
|
-
// Evict on settle (success or failure) so the key never replays a stale
|
|
115
|
-
// outcome. Two-arg `then` keeps this cleanup branch from surfacing the
|
|
116
|
-
// rejection as unhandled — the main chain below owns the user-facing
|
|
117
|
-
// failure handling.
|
|
118
|
-
const evict = ()=>connectRequests.delete(key);
|
|
119
|
-
request.then(evict, evict);
|
|
120
|
-
connectRequests.set(key, request);
|
|
121
|
-
}
|
|
107
|
+
})));
|
|
122
108
|
request.then(async ()=>{
|
|
123
109
|
clearSentryInstallWorkspace(window.sessionStorage);
|
|
124
110
|
await queryClient.invalidateQueries({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import type {SentryConnectResponseDto} from '@shipfox/api-integration-sentry-dto';\nimport {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {connectSentry, integrationsQueryKeys} from '#hooks/api/integrations.js';\nimport {\n classifySentryConnectError,\n clearSentryInstallWorkspace,\n parseSentryCallbackParams,\n preselectSentryWorkspace,\n readSentryInstallWorkspace,\n type SentryConnectFailure,\n} from '#sentry-callback.js';\n\n// De-dupes concurrent attempts for the same installation+workspace+code\n// (double click, remount while in flight). Entries are evicted once the\n// request settles, so Retry, and any later attempt carrying a fresh grant\n// code, always issues a genuinely new request instead of replaying a cached\n// outcome.\nconst connectRequests = new Map<string, Promise<SentryConnectResponseDto>>();\n\nexport function SentryCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const queryClient = useQueryClient();\n\n const params = useMemo(() => parseSentryCallbackParams(search), [search]);\n const storedWorkspaceId = useMemo(() => readSentryInstallWorkspace(window.sessionStorage), []);\n const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);\n const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;\n\n const [connectingId, setConnectingId] = useState<string | undefined>();\n const [failure, setFailure] = useState<\n {workspaceId: string; failure: SentryConnectFailure} | undefined\n >();\n const [retryLocked, setRetryLocked] = useState(false);\n const disposedRef = useRef(false);\n\n useEffect(() => {\n disposedRef.current = false;\n return () => {\n disposedRef.current = true;\n };\n }, []);\n\n // Wait for auth before treating an empty workspace list as \"no workspace\": on\n // a cold return from Sentry, `workspaces` is [] until the session refresh\n // resolves, and bouncing to `/` then would discard the one-shot grant code.\n const noWorkspace = !isLoading && preselection.kind === 'none';\n useEffect(() => {\n if (!noWorkspace) return;\n toast.error('You need a workspace before installing Sentry.');\n navigate({to: '/', replace: true});\n }, [noWorkspace, navigate]);\n\n const retryAfterSeconds =\n failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;\n useEffect(() => {\n // No backoff window (any non-rate-limited failure) re-enables Retry, so a\n // prior rate-limit lock can never strand the button in a disabled state.\n if (!retryAfterSeconds) {\n setRetryLocked(false);\n return;\n }\n setRetryLocked(true);\n const timer = setTimeout(() => setRetryLocked(false), retryAfterSeconds * 1000);\n return () => clearTimeout(timer);\n }, [retryAfterSeconds]);\n\n if (!params) {\n return (\n <CallbackColumn>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n This Sentry link is missing required parameters. Start the install again from your\n workspace settings.\n </Text>\n </Callout>\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n </CallbackColumn>\n );\n }\n\n // Hold the loader until auth resolves; the workspace list and the\n // noWorkspace redirect both depend on a settled `workspaces`.\n if (isLoading) return <FullPageLoader />;\n\n if (noWorkspace) return null;\n\n const connect = (workspaceId: string) => {\n setConnectingId(workspaceId);\n setFailure(undefined);\n const key = `${params.installationId}|${workspaceId}|${params.code}`;\n let request = connectRequests.get(key);\n if (!request) {\n request = refreshAuth().then((session) =>\n connectSentry({\n body: {\n workspace_id: workspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token: session.token,\n }),\n );\n // Evict on settle (success or failure) so the key never replays a stale\n // outcome. Two-arg `then` keeps this cleanup branch from surfacing the\n // rejection as unhandled — the main chain below owns the user-facing\n // failure handling.\n const evict = () => connectRequests.delete(key);\n request.then(evict, evict);\n connectRequests.set(key, request);\n }\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(window.sessionStorage);\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),\n });\n if (disposedRef.current) return;\n toast.success('Sentry installed.');\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n })\n .catch((error: unknown) => {\n if (disposedRef.current) return;\n const classified = classifySentryConnectError(error);\n if (classified.kind === 'terminal') {\n // Only a fresh install (new grant code) can recover; the stored\n // handoff has served its purpose either way.\n clearSentryInstallWorkspace(window.sessionStorage);\n }\n setConnectingId(undefined);\n setFailure({workspaceId, failure: classified});\n });\n };\n\n const orderedWorkspaces = preselectedId\n ? [\n ...workspaces.filter((workspace) => workspace.id === preselectedId),\n ...workspaces.filter((workspace) => workspace.id !== preselectedId),\n ]\n : workspaces;\n\n const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;\n\n return (\n <CallbackColumn>\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h2\">Install Sentry</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {params.orgSlug\n ? `Install the Sentry org \"${params.orgSlug}\" in a workspace.`\n : 'Install this Sentry integration in a workspace.'}\n </Text>\n </header>\n\n {failure ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex flex-col gap-8\">\n <Text size=\"sm\">{failure.failure.message}</Text>\n {failure.failure.kind === 'retryable' ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n disabled={retryLocked || connectingId !== undefined}\n onClick={() => connect(failure.workspaceId)}\n >\n Retry\n </Button>\n ) : null}\n {failure.failure.kind === 'terminal' && failure.failure.startOver ? (\n <Button asChild size=\"sm\" variant=\"secondary\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/integrations/sentry\" params={{wid: failure.workspaceId}}>\n Start over\n </Link>\n </Button>\n ) : null}\n </div>\n </Callout>\n ) : null}\n\n <section className=\"flex flex-col gap-8\" aria-label=\"Choose a workspace\">\n {orderedWorkspaces.map((workspace) => (\n <Card key={workspace.id} className=\"p-16\">\n <div className=\"flex items-center justify-between gap-12\">\n <Text size=\"md\" bold className=\"truncate\">\n {workspace.name}\n </Text>\n <Button\n variant=\"secondary\"\n disabled={connectingId !== undefined}\n isLoading={connectingId === workspace.id}\n onClick={() => connect(workspace.id)}\n >\n Install\n </Button>\n </div>\n </Card>\n ))}\n </section>\n\n {failureWorkspaceId ? (\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: failureWorkspaceId}}>\n Back to settings\n </Link>\n </ButtonLink>\n ) : null}\n </CallbackColumn>\n );\n}\n\nfunction CallbackColumn({children}: {children: React.ReactNode}) {\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n {children}\n </div>\n </main>\n );\n}\n"],"names":["useAuthState","useRefreshAuth","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","connectSentry","integrationsQueryKeys","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","Map","SentryCallbackPage","search","strict","navigate","refreshAuth","workspaces","isLoading","queryClient","params","storedWorkspaceId","window","sessionStorage","preselection","preselectedId","kind","undefined","connectingId","setConnectingId","failure","setFailure","retryLocked","setRetryLocked","disposedRef","current","noWorkspace","error","to","replace","retryAfterSeconds","timer","setTimeout","clearTimeout","CallbackColumn","role","type","size","asChild","variant","className","connect","workspaceId","key","installationId","code","request","get","then","session","body","workspace_id","installation_id","token","evict","delete","set","invalidateQueries","queryKey","connectionsByWorkspace","success","wid","catch","classified","orderedWorkspaces","filter","workspace","id","failureWorkspaceId","header","orgSlug","div","message","disabled","onClick","startOver","section","aria-label","map","bold","name","children","main"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/sentry-callback-page.tsx"],"sourcesContent":["import type {SentryConnectResponseDto} from '@shipfox/api-integration-sentry-dto';\nimport {useAuthState, useRefreshAuth} from '@shipfox/client-auth';\nimport {createSingleFlight} from '@shipfox/client-ui';\nimport {Button, ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {Link, useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useRef, useState} from 'react';\nimport {connectSentry, integrationsQueryKeys} from '#hooks/api/integrations.js';\nimport {\n classifySentryConnectError,\n clearSentryInstallWorkspace,\n parseSentryCallbackParams,\n preselectSentryWorkspace,\n readSentryInstallWorkspace,\n type SentryConnectFailure,\n} from '#sentry-callback.js';\n\nconst connectRequests = createSingleFlight<string, SentryConnectResponseDto>();\n\nexport function SentryCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const {workspaces, isLoading} = useAuthState();\n const queryClient = useQueryClient();\n\n const params = useMemo(() => parseSentryCallbackParams(search), [search]);\n const storedWorkspaceId = useMemo(() => readSentryInstallWorkspace(window.sessionStorage), []);\n const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);\n const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;\n\n const [connectingId, setConnectingId] = useState<string | undefined>();\n const [failure, setFailure] = useState<\n {workspaceId: string; failure: SentryConnectFailure} | undefined\n >();\n const [retryLocked, setRetryLocked] = useState(false);\n const disposedRef = useRef(false);\n\n useEffect(() => {\n disposedRef.current = false;\n return () => {\n disposedRef.current = true;\n };\n }, []);\n\n // Wait for auth before treating an empty workspace list as \"no workspace\": on\n // a cold return from Sentry, `workspaces` is [] until the session refresh\n // resolves, and bouncing to `/` then would discard the one-shot grant code.\n const noWorkspace = !isLoading && preselection.kind === 'none';\n useEffect(() => {\n if (!noWorkspace) return;\n toast.error('You need a workspace before installing Sentry.');\n navigate({to: '/', replace: true});\n }, [noWorkspace, navigate]);\n\n const retryAfterSeconds =\n failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;\n useEffect(() => {\n // No backoff window (any non-rate-limited failure) re-enables Retry, so a\n // prior rate-limit lock can never strand the button in a disabled state.\n if (!retryAfterSeconds) {\n setRetryLocked(false);\n return;\n }\n setRetryLocked(true);\n const timer = setTimeout(() => setRetryLocked(false), retryAfterSeconds * 1000);\n return () => clearTimeout(timer);\n }, [retryAfterSeconds]);\n\n if (!params) {\n return (\n <CallbackColumn>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n This Sentry link is missing required parameters. Start the install again from your\n workspace settings.\n </Text>\n </Callout>\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n </CallbackColumn>\n );\n }\n\n // Hold the loader until auth resolves; the workspace list and the\n // noWorkspace redirect both depend on a settled `workspaces`.\n if (isLoading) return <FullPageLoader />;\n\n if (noWorkspace) return null;\n\n const connect = (workspaceId: string) => {\n setConnectingId(workspaceId);\n setFailure(undefined);\n const key = `${params.installationId}|${workspaceId}|${params.code}`;\n const request = connectRequests.run(\n key,\n async () =>\n await refreshAuth().then((session) =>\n connectSentry({\n body: {\n workspace_id: workspaceId,\n code: params.code,\n installation_id: params.installationId,\n },\n token: session.token,\n }),\n ),\n );\n\n request\n .then(async () => {\n clearSentryInstallWorkspace(window.sessionStorage);\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId),\n });\n if (disposedRef.current) return;\n toast.success('Sentry installed.');\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: workspaceId},\n replace: true,\n });\n })\n .catch((error: unknown) => {\n if (disposedRef.current) return;\n const classified = classifySentryConnectError(error);\n if (classified.kind === 'terminal') {\n // Only a fresh install (new grant code) can recover; the stored\n // handoff has served its purpose either way.\n clearSentryInstallWorkspace(window.sessionStorage);\n }\n setConnectingId(undefined);\n setFailure({workspaceId, failure: classified});\n });\n };\n\n const orderedWorkspaces = preselectedId\n ? [\n ...workspaces.filter((workspace) => workspace.id === preselectedId),\n ...workspaces.filter((workspace) => workspace.id !== preselectedId),\n ]\n : workspaces;\n\n const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;\n\n return (\n <CallbackColumn>\n <header className=\"flex flex-col gap-8\">\n <Header variant=\"h2\">Install Sentry</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n {params.orgSlug\n ? `Install the Sentry org \"${params.orgSlug}\" in a workspace.`\n : 'Install this Sentry integration in a workspace.'}\n </Text>\n </header>\n\n {failure ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex flex-col gap-8\">\n <Text size=\"sm\">{failure.failure.message}</Text>\n {failure.failure.kind === 'retryable' ? (\n <Button\n size=\"sm\"\n variant=\"secondary\"\n className=\"w-fit\"\n disabled={retryLocked || connectingId !== undefined}\n onClick={() => connect(failure.workspaceId)}\n >\n Retry\n </Button>\n ) : null}\n {failure.failure.kind === 'terminal' && failure.failure.startOver ? (\n <Button asChild size=\"sm\" variant=\"secondary\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/integrations/sentry\" params={{wid: failure.workspaceId}}>\n Start over\n </Link>\n </Button>\n ) : null}\n </div>\n </Callout>\n ) : null}\n\n <section className=\"flex flex-col gap-8\" aria-label=\"Choose a workspace\">\n {orderedWorkspaces.map((workspace) => (\n <Card key={workspace.id} className=\"p-16\">\n <div className=\"flex items-center justify-between gap-12\">\n <Text size=\"md\" bold className=\"truncate\">\n {workspace.name}\n </Text>\n <Button\n variant=\"secondary\"\n disabled={connectingId !== undefined}\n isLoading={connectingId === workspace.id}\n onClick={() => connect(workspace.id)}\n >\n Install\n </Button>\n </div>\n </Card>\n ))}\n </section>\n\n {failureWorkspaceId ? (\n <ButtonLink asChild variant=\"muted\" className=\"w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: failureWorkspaceId}}>\n Back to settings\n </Link>\n </ButtonLink>\n ) : null}\n </CallbackColumn>\n );\n}\n\nfunction CallbackColumn({children}: {children: React.ReactNode}) {\n return (\n <main className=\"flex min-h-screen bg-background-subtle-base px-16 py-32\">\n <div className=\"mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20\">\n {children}\n </div>\n </main>\n );\n}\n"],"names":["useAuthState","useRefreshAuth","createSingleFlight","Button","ButtonLink","Callout","Card","FullPageLoader","toast","Header","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","connectSentry","integrationsQueryKeys","classifySentryConnectError","clearSentryInstallWorkspace","parseSentryCallbackParams","preselectSentryWorkspace","readSentryInstallWorkspace","connectRequests","SentryCallbackPage","search","strict","navigate","refreshAuth","workspaces","isLoading","queryClient","params","storedWorkspaceId","window","sessionStorage","preselection","preselectedId","kind","undefined","connectingId","setConnectingId","failure","setFailure","retryLocked","setRetryLocked","disposedRef","current","noWorkspace","error","to","replace","retryAfterSeconds","timer","setTimeout","clearTimeout","CallbackColumn","role","type","size","asChild","variant","className","connect","workspaceId","key","installationId","code","request","run","then","session","body","workspace_id","installation_id","token","invalidateQueries","queryKey","connectionsByWorkspace","success","wid","catch","classified","orderedWorkspaces","filter","workspace","id","failureWorkspaceId","header","orgSlug","div","message","disabled","onClick","startOver","section","aria-label","map","bold","name","children","main"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slack-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/slack-callback-page.tsx"],"names":[],"mappings":"AA2BA,wBAAgB,iBAAiB,gCA+FhC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
3
|
+
import { createSingleFlight } from '@shipfox/client-ui';
|
|
3
4
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
4
5
|
import { toast } from '@shipfox/react-ui/toast';
|
|
5
6
|
import { useQueryClient } from '@tanstack/react-query';
|
|
@@ -8,9 +9,11 @@ import { useEffect, useMemo, useState } from 'react';
|
|
|
8
9
|
import { CallbackStatusShell } from '#components/callback-status-shell.js';
|
|
9
10
|
import { integrationsQueryKeys, useCompleteSlackCallbackMutation } from '#hooks/api/integrations.js';
|
|
10
11
|
import { classifySlackCallbackError, clearSlackInstallWorkspace, parseSlackCallbackQuery, readSlackInstallWorkspace, serializeSlackCallbackQuery } from '#slack-callback.js';
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
const callbackRequests =
|
|
12
|
+
// Retain only recent completions: this bounds long-lived callback pages while
|
|
13
|
+
// still covering StrictMode and immediate Back/Forward remounts.
|
|
14
|
+
const callbackRequests = createSingleFlight({
|
|
15
|
+
maxTerminalResults: 32
|
|
16
|
+
});
|
|
14
17
|
const completedCallbacks = new Set();
|
|
15
18
|
const toastedCallbacks = new Set();
|
|
16
19
|
export function SlackCallbackPage() {
|
|
@@ -31,14 +34,10 @@ export function SlackCallbackPage() {
|
|
|
31
34
|
if (!params) return;
|
|
32
35
|
let disposed = false;
|
|
33
36
|
const key = serializeSlackCallbackQuery(params);
|
|
34
|
-
|
|
35
|
-
if (!request) {
|
|
36
|
-
request = refreshAuth().then(async (session)=>await completeSlackCallback({
|
|
37
|
+
const request = callbackRequests.run(key, async ()=>await refreshAuth().then(async (session)=>await completeSlackCallback({
|
|
37
38
|
query: params,
|
|
38
39
|
token: session.token
|
|
39
|
-
}));
|
|
40
|
-
callbackRequests.set(key, request);
|
|
41
|
-
}
|
|
40
|
+
})));
|
|
42
41
|
request.then(async (connection)=>{
|
|
43
42
|
if (disposed) return;
|
|
44
43
|
if (completedCallbacks.has(key)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/slack-callback-page.tsx"],"sourcesContent":["import type {SlackCallbackResponseDto} from '@shipfox/api-integration-slack-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, useCompleteSlackCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifySlackCallbackError,\n clearSlackInstallWorkspace,\n parseSlackCallbackQuery,\n readSlackInstallWorkspace,\n type SlackCallbackFailure,\n serializeSlackCallbackQuery,\n} from '#slack-callback.js';\n\n//
|
|
1
|
+
{"version":3,"sources":["../../src/pages/slack-callback-page.tsx"],"sourcesContent":["import type {SlackCallbackResponseDto} from '@shipfox/api-integration-slack-dto';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {createSingleFlight} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useQueryClient} from '@tanstack/react-query';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect, useMemo, useState} from 'react';\nimport {CallbackStatusShell} from '#components/callback-status-shell.js';\nimport {integrationsQueryKeys, useCompleteSlackCallbackMutation} from '#hooks/api/integrations.js';\nimport {\n classifySlackCallbackError,\n clearSlackInstallWorkspace,\n parseSlackCallbackQuery,\n readSlackInstallWorkspace,\n type SlackCallbackFailure,\n serializeSlackCallbackQuery,\n} from '#slack-callback.js';\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, SlackCallbackResponseDto>({\n maxTerminalResults: 32,\n});\nconst completedCallbacks = new Set<string>();\nconst toastedCallbacks = new Set<string>();\n\nexport function SlackCallbackPage() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n const queryClient = useQueryClient();\n const {mutateAsync: completeSlackCallback} = useCompleteSlackCallbackMutation();\n const params = useMemo(() => parseSlackCallbackQuery(search), [search]);\n const workspaceId = useMemo(() => readSlackInstallWorkspace(window.sessionStorage), []);\n const [failure, setFailure] = useState<SlackCallbackFailure>();\n const [completedWorkspaceId, setCompletedWorkspaceId] = useState<string>();\n\n useEffect(() => {\n if (!params) return;\n let disposed = false;\n const key = serializeSlackCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await refreshAuth().then(\n async (session) => await completeSlackCallback({query: params, token: session.token}),\n ),\n );\n request.then(\n async (connection) => {\n if (disposed) return;\n if (completedCallbacks.has(key)) {\n setCompletedWorkspaceId(connection.workspace_id);\n return;\n }\n completedCallbacks.add(key);\n try {\n clearSlackInstallWorkspace(window.sessionStorage);\n } catch {\n // The successful API response remains the source of truth.\n }\n try {\n await queryClient.invalidateQueries({\n queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id),\n });\n } catch {\n // Navigation can continue when cache refresh is unavailable.\n }\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.success('Slack installed.');\n }\n try {\n await navigate({\n to: '/workspaces/$wid/settings/integrations',\n params: {wid: connection.workspace_id},\n replace: true,\n });\n } catch {\n if (!disposed) setCompletedWorkspaceId(connection.workspace_id);\n }\n },\n (error: unknown) => {\n if (!disposed) setFailure(classifySlackCallbackError(error));\n },\n );\n return () => {\n disposed = true;\n };\n }, [completeSlackCallback, navigate, params, queryClient, refreshAuth]);\n\n if (!params)\n return (\n <CallbackStatusShell\n title=\"Invalid Slack callback\"\n message=\"This Slack link is missing required parameters. Start the install again from workspace settings.\"\n startOver\n workspaceId={workspaceId}\n installPath=\"/workspaces/$wid/integrations/slack\"\n />\n );\n if (completedWorkspaceId)\n return (\n <CallbackStatusShell\n title=\"Slack connected\"\n message=\"Slack is connected. Continue in integrations settings.\"\n workspaceId={completedWorkspaceId}\n installPath=\"/workspaces/$wid/integrations/slack\"\n />\n );\n if (failure)\n return (\n <CallbackStatusShell\n {...failure}\n workspaceId={workspaceId}\n switchAccount={failure.signIn}\n installPath=\"/workspaces/$wid/integrations/slack\"\n />\n );\n return <FullPageLoader aria-label=\"Connecting Slack\" />;\n}\n"],"names":["useRefreshAuth","createSingleFlight","FullPageLoader","toast","useQueryClient","useNavigate","useSearch","useEffect","useMemo","useState","CallbackStatusShell","integrationsQueryKeys","useCompleteSlackCallbackMutation","classifySlackCallbackError","clearSlackInstallWorkspace","parseSlackCallbackQuery","readSlackInstallWorkspace","serializeSlackCallbackQuery","callbackRequests","maxTerminalResults","completedCallbacks","Set","toastedCallbacks","SlackCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeSlackCallback","params","workspaceId","window","sessionStorage","failure","setFailure","completedWorkspaceId","setCompletedWorkspaceId","disposed","key","request","run","then","session","query","token","connection","has","workspace_id","add","invalidateQueries","queryKey","connectionsByWorkspace","success","to","wid","replace","error","title","message","startOver","installPath","switchAccount","signIn","aria-label"],"mappings":"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"}
|
|
@@ -3,4 +3,5 @@ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
|
|
|
3
3
|
}>;
|
|
4
4
|
export default _default;
|
|
5
5
|
declare function GithubCallbackRoute(): import("react").JSX.Element;
|
|
6
|
+
export declare function githubCallbackErrorMessage(error: unknown): string;
|
|
6
7
|
//# sourceMappingURL=github-callback.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"github-callback.d.ts","sourceRoot":"","sources":["../../src/routes/github-callback.tsx"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"github-callback.d.ts","sourceRoot":"","sources":["../../src/routes/github-callback.tsx"],"names":[],"mappings":";;;AAqBA,wBAA6D;AAE7D,iBAAS,mBAAmB,gCAsD3B;AAoCD,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAGjE"}
|
|
@@ -2,11 +2,16 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { ApiError, apiRequest } from '@shipfox/client-api';
|
|
3
3
|
import { useRefreshAuth } from '@shipfox/client-auth';
|
|
4
4
|
import { defineRoute } from '@shipfox/client-shell/runtime';
|
|
5
|
+
import { createSingleFlight } from '@shipfox/client-ui';
|
|
5
6
|
import { FullPageLoader } from '@shipfox/react-ui/loader';
|
|
6
7
|
import { toast } from '@shipfox/react-ui/toast';
|
|
7
8
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
|
8
9
|
import { useEffect } from 'react';
|
|
9
|
-
|
|
10
|
+
// Retain only recent completions: this bounds long-lived callback pages while
|
|
11
|
+
// still covering StrictMode and immediate Back/Forward remounts.
|
|
12
|
+
const callbackRequests = createSingleFlight({
|
|
13
|
+
maxTerminalResults: 32
|
|
14
|
+
});
|
|
10
15
|
const toastedCallbacks = new Set();
|
|
11
16
|
export default defineRoute({
|
|
12
17
|
component: GithubCallbackRoute
|
|
@@ -38,14 +43,13 @@ function GithubCallbackRoute() {
|
|
|
38
43
|
}
|
|
39
44
|
let disposed = false;
|
|
40
45
|
const key = encodeCallbackQuery(params);
|
|
41
|
-
const request = callbackRequests.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
callbackRequests.set(key, request);
|
|
46
|
+
const request = callbackRequests.run(key, async ()=>await refreshAuth().then(async (session)=>{
|
|
47
|
+
await apiRequest(`/integrations/github/callback/api?${key}`, {
|
|
48
|
+
headers: {
|
|
49
|
+
authorization: `Bearer ${session.token}`
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}));
|
|
49
53
|
request.then(()=>{
|
|
50
54
|
if (disposed) return;
|
|
51
55
|
if (!toastedCallbacks.has(key)) {
|
|
@@ -120,9 +124,8 @@ function numberParam(value) {
|
|
|
120
124
|
}
|
|
121
125
|
return undefined;
|
|
122
126
|
}
|
|
123
|
-
function githubCallbackErrorMessage(error) {
|
|
124
|
-
if (error instanceof ApiError) return
|
|
125
|
-
if (error instanceof Error) return error.message;
|
|
127
|
+
export function githubCallbackErrorMessage(error) {
|
|
128
|
+
if (error instanceof ApiError) return 'GitHub could not be installed. Try again from settings.';
|
|
126
129
|
return 'Could not install GitHub.';
|
|
127
130
|
}
|
|
128
131
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/routes/github-callback.tsx"],"sourcesContent":["import {ApiError, apiRequest} from '@shipfox/client-api';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {defineRoute} from '@shipfox/client-shell/runtime';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect} from 'react';\n\ninterface GithubCallbackParams {\n code: string;\n installationId: number;\n state: string;\n setupAction?: string;\n}\n\nconst callbackRequests =
|
|
1
|
+
{"version":3,"sources":["../../src/routes/github-callback.tsx"],"sourcesContent":["import {ApiError, apiRequest} from '@shipfox/client-api';\nimport {useRefreshAuth} from '@shipfox/client-auth';\nimport {defineRoute} from '@shipfox/client-shell/runtime';\nimport {createSingleFlight} from '@shipfox/client-ui';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {useNavigate, useSearch} from '@tanstack/react-router';\nimport {useEffect} from 'react';\n\ninterface GithubCallbackParams {\n code: string;\n installationId: number;\n state: string;\n setupAction?: string;\n}\n\n// Retain only recent completions: this bounds long-lived callback pages while\n// still covering StrictMode and immediate Back/Forward remounts.\nconst callbackRequests = createSingleFlight<string, void>({maxTerminalResults: 32});\nconst toastedCallbacks = new Set<string>();\n\nexport default defineRoute({component: GithubCallbackRoute});\n\nfunction GithubCallbackRoute() {\n const search = useSearch({strict: false});\n const navigate = useNavigate();\n const refreshAuth = useRefreshAuth();\n\n useEffect(() => {\n const missing = missingCallbackParams(search);\n if (missing.length > 0) {\n toast.error(\n `GitHub callback is missing: ${missing.join(', ')}. ${missing.includes('code') ? 'Enable \"Request user authorization (OAuth) during installation\" on the Shipfox GitHub App.' : ''}`,\n );\n navigate({to: '/', replace: true});\n return;\n }\n const params = githubCallbackParams(search);\n if (!params) {\n toast.error('GitHub callback is missing required parameters.');\n navigate({to: '/', replace: true});\n return;\n }\n let disposed = false;\n const key = encodeCallbackQuery(params);\n const request = callbackRequests.run(\n key,\n async () =>\n await refreshAuth().then(async (session) => {\n await apiRequest(`/integrations/github/callback/api?${key}`, {\n headers: {authorization: `Bearer ${session.token}`},\n });\n }),\n );\n request\n .then(() => {\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.success('GitHub installed.');\n }\n navigate({to: '/', replace: true});\n })\n .catch((error: unknown) => {\n if (disposed) return;\n if (!toastedCallbacks.has(key)) {\n toastedCallbacks.add(key);\n toast.error(githubCallbackErrorMessage(error));\n }\n navigate({to: '/', replace: true});\n });\n return () => {\n disposed = true;\n };\n }, [navigate, refreshAuth, search]);\n\n return <FullPageLoader />;\n}\n\nfunction missingCallbackParams(search: Record<string, unknown>): string[] {\n const missing: string[] = [];\n if (!stringParam(search.code)) missing.push('code');\n if (!numberParam(search.installation_id)) missing.push('installation_id');\n if (!stringParam(search.state)) missing.push('state');\n return missing;\n}\nfunction githubCallbackParams(search: Record<string, unknown>): GithubCallbackParams | undefined {\n const code = stringParam(search.code);\n const installationId = numberParam(search.installation_id);\n const state = stringParam(search.state);\n if (!code || installationId === undefined || !state) return undefined;\n const setupAction = stringParam(search.setup_action);\n return setupAction ? {code, installationId, state, setupAction} : {code, installationId, state};\n}\nfunction encodeCallbackQuery(params: GithubCallbackParams): string {\n const search = new URLSearchParams();\n search.set('code', params.code);\n search.set('installation_id', params.installationId.toString());\n search.set('state', params.state);\n if (params.setupAction) search.set('setup_action', params.setupAction);\n return search.toString();\n}\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\nfunction numberParam(value: unknown): number | undefined {\n if (typeof value === 'number' && Number.isFinite(value)) return value;\n if (typeof value === 'string' && value.length > 0) {\n const parsed = Number(value);\n return Number.isFinite(parsed) ? parsed : undefined;\n }\n return undefined;\n}\nexport function githubCallbackErrorMessage(error: unknown): string {\n if (error instanceof ApiError) return 'GitHub could not be installed. Try again from settings.';\n return 'Could not install GitHub.';\n}\n"],"names":["ApiError","apiRequest","useRefreshAuth","defineRoute","createSingleFlight","FullPageLoader","toast","useNavigate","useSearch","useEffect","callbackRequests","maxTerminalResults","toastedCallbacks","Set","component","GithubCallbackRoute","search","strict","navigate","refreshAuth","missing","missingCallbackParams","length","error","join","includes","to","replace","params","githubCallbackParams","disposed","key","encodeCallbackQuery","request","run","then","session","headers","authorization","token","has","add","success","catch","githubCallbackErrorMessage","stringParam","code","push","numberParam","installation_id","state","installationId","undefined","setupAction","setup_action","URLSearchParams","set","toString","value","Number","isFinite","parsed"],"mappings":"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"}
|