@shipfox/client-integrations 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 (243) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +4 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +207 -0
  8. package/LICENSE +21 -0
  9. package/dist/components/connection-picker.d.ts +7 -0
  10. package/dist/components/connection-picker.d.ts.map +1 -0
  11. package/dist/components/connection-picker.js +98 -0
  12. package/dist/components/connection-picker.js.map +1 -0
  13. package/dist/components/installed-integrations-section.d.ts +14 -0
  14. package/dist/components/installed-integrations-section.d.ts.map +1 -0
  15. package/dist/components/installed-integrations-section.js +203 -0
  16. package/dist/components/installed-integrations-section.js.map +1 -0
  17. package/dist/components/integration-delete-confirm-modal.d.ts +10 -0
  18. package/dist/components/integration-delete-confirm-modal.d.ts.map +1 -0
  19. package/dist/components/integration-delete-confirm-modal.js +61 -0
  20. package/dist/components/integration-delete-confirm-modal.js.map +1 -0
  21. package/dist/components/integration-gallery-for-workspace.d.ts +9 -0
  22. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -0
  23. package/dist/components/integration-gallery-for-workspace.js +169 -0
  24. package/dist/components/integration-gallery-for-workspace.js.map +1 -0
  25. package/dist/components/integration-gallery.d.ts +8 -0
  26. package/dist/components/integration-gallery.d.ts.map +1 -0
  27. package/dist/components/integration-gallery.js +26 -0
  28. package/dist/components/integration-gallery.js.map +1 -0
  29. package/dist/components/integration-gallery.stories.d.ts +26 -0
  30. package/dist/components/integration-gallery.stories.d.ts.map +1 -0
  31. package/dist/components/integration-gallery.stories.js +335 -0
  32. package/dist/components/integration-gallery.stories.js.map +1 -0
  33. package/dist/components/integration-usage-events.d.ts +4 -0
  34. package/dist/components/integration-usage-events.d.ts.map +1 -0
  35. package/dist/components/integration-usage-events.js +127 -0
  36. package/dist/components/integration-usage-events.js.map +1 -0
  37. package/dist/components/integration-usage-modal.d.ts +16 -0
  38. package/dist/components/integration-usage-modal.d.ts.map +1 -0
  39. package/dist/components/integration-usage-modal.js +184 -0
  40. package/dist/components/integration-usage-modal.js.map +1 -0
  41. package/dist/components/provider-grid.d.ts +14 -0
  42. package/dist/components/provider-grid.d.ts.map +1 -0
  43. package/dist/components/provider-grid.js +152 -0
  44. package/dist/components/provider-grid.js.map +1 -0
  45. package/dist/components/redirect-install-page.d.ts +19 -0
  46. package/dist/components/redirect-install-page.d.ts.map +1 -0
  47. package/dist/components/redirect-install-page.js +68 -0
  48. package/dist/components/redirect-install-page.js.map +1 -0
  49. package/dist/components/repository-picker.d.ts +15 -0
  50. package/dist/components/repository-picker.d.ts.map +1 -0
  51. package/dist/components/repository-picker.js +78 -0
  52. package/dist/components/repository-picker.js.map +1 -0
  53. package/dist/components/webhook/copyable-value.d.ts +10 -0
  54. package/dist/components/webhook/copyable-value.d.ts.map +1 -0
  55. package/dist/components/webhook/copyable-value.js +65 -0
  56. package/dist/components/webhook/copyable-value.js.map +1 -0
  57. package/dist/components/webhook/webhook-create-modal.d.ts +10 -0
  58. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -0
  59. package/dist/components/webhook/webhook-create-modal.js +233 -0
  60. package/dist/components/webhook/webhook-create-modal.js.map +1 -0
  61. package/dist/components/webhook/webhook-form-errors.d.ts +11 -0
  62. package/dist/components/webhook/webhook-form-errors.d.ts.map +1 -0
  63. package/dist/components/webhook/webhook-form-errors.js +27 -0
  64. package/dist/components/webhook/webhook-form-errors.js.map +1 -0
  65. package/dist/components/webhook/webhook-modals.stories.d.ts +25 -0
  66. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -0
  67. package/dist/components/webhook/webhook-modals.stories.js +222 -0
  68. package/dist/components/webhook/webhook-modals.stories.js.map +1 -0
  69. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts +2 -0
  70. package/dist/components/webhook/webhook-public-endpoint-alert.d.ts.map +1 -0
  71. package/dist/components/webhook/webhook-public-endpoint-alert.js +20 -0
  72. package/dist/components/webhook/webhook-public-endpoint-alert.js.map +1 -0
  73. package/dist/components/webhook/webhook-usage-details.d.ts +7 -0
  74. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -0
  75. package/dist/components/webhook/webhook-usage-details.js +49 -0
  76. package/dist/components/webhook/webhook-usage-details.js.map +1 -0
  77. package/dist/connection-status-badge.d.ts +12 -0
  78. package/dist/connection-status-badge.d.ts.map +1 -0
  79. package/dist/connection-status-badge.js +34 -0
  80. package/dist/connection-status-badge.js.map +1 -0
  81. package/dist/feature.d.ts +37 -0
  82. package/dist/feature.d.ts.map +1 -0
  83. package/dist/feature.js +48 -0
  84. package/dist/feature.js.map +1 -0
  85. package/dist/hooks/api/integrations.d.ts +265 -0
  86. package/dist/hooks/api/integrations.d.ts.map +1 -0
  87. package/dist/hooks/api/integrations.js +229 -0
  88. package/dist/hooks/api/integrations.js.map +1 -0
  89. package/dist/hooks/api/webhook-connections.d.ts +91 -0
  90. package/dist/hooks/api/webhook-connections.d.ts.map +1 -0
  91. package/dist/hooks/api/webhook-connections.js +97 -0
  92. package/dist/hooks/api/webhook-connections.js.map +1 -0
  93. package/dist/index.d.ts +17 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +18 -0
  96. package/dist/index.js.map +1 -0
  97. package/dist/integration-icon.d.ts +17 -0
  98. package/dist/integration-icon.d.ts.map +1 -0
  99. package/dist/integration-icon.js +22 -0
  100. package/dist/integration-icon.js.map +1 -0
  101. package/dist/linear-callback.d.ts +17 -0
  102. package/dist/linear-callback.d.ts.map +1 -0
  103. package/dist/linear-callback.js +113 -0
  104. package/dist/linear-callback.js.map +1 -0
  105. package/dist/pages/gitea-form-errors.d.ts +11 -0
  106. package/dist/pages/gitea-form-errors.d.ts.map +1 -0
  107. package/dist/pages/gitea-form-errors.js +35 -0
  108. package/dist/pages/gitea-form-errors.js.map +1 -0
  109. package/dist/pages/gitea-install-page.d.ts +2 -0
  110. package/dist/pages/gitea-install-page.d.ts.map +1 -0
  111. package/dist/pages/gitea-install-page.js +139 -0
  112. package/dist/pages/gitea-install-page.js.map +1 -0
  113. package/dist/pages/github-install-page.d.ts +2 -0
  114. package/dist/pages/github-install-page.d.ts.map +1 -0
  115. package/dist/pages/github-install-page.js +11 -0
  116. package/dist/pages/github-install-page.js.map +1 -0
  117. package/dist/pages/linear-callback-page.d.ts +2 -0
  118. package/dist/pages/linear-callback-page.d.ts.map +1 -0
  119. package/dist/pages/linear-callback-page.js +192 -0
  120. package/dist/pages/linear-callback-page.js.map +1 -0
  121. package/dist/pages/linear-install-page.d.ts +2 -0
  122. package/dist/pages/linear-install-page.d.ts.map +1 -0
  123. package/dist/pages/linear-install-page.js +24 -0
  124. package/dist/pages/linear-install-page.js.map +1 -0
  125. package/dist/pages/sentry-callback-page.d.ts +2 -0
  126. package/dist/pages/sentry-callback-page.d.ts.map +1 -0
  127. package/dist/pages/sentry-callback-page.js +256 -0
  128. package/dist/pages/sentry-callback-page.js.map +1 -0
  129. package/dist/pages/sentry-install-page.d.ts +2 -0
  130. package/dist/pages/sentry-install-page.d.ts.map +1 -0
  131. package/dist/pages/sentry-install-page.js +17 -0
  132. package/dist/pages/sentry-install-page.js.map +1 -0
  133. package/dist/pages/source-control-onboarding-page.d.ts +2 -0
  134. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -0
  135. package/dist/pages/source-control-onboarding-page.js +37 -0
  136. package/dist/pages/source-control-onboarding-page.js.map +1 -0
  137. package/dist/provider-catalog.d.ts +15 -0
  138. package/dist/provider-catalog.d.ts.map +1 -0
  139. package/dist/provider-catalog.js +28 -0
  140. package/dist/provider-catalog.js.map +1 -0
  141. package/dist/routes/gitea.d.ts +6 -0
  142. package/dist/routes/gitea.d.ts.map +1 -0
  143. package/dist/routes/gitea.js +7 -0
  144. package/dist/routes/gitea.js.map +1 -0
  145. package/dist/routes/github-callback.d.ts +6 -0
  146. package/dist/routes/github-callback.d.ts.map +1 -0
  147. package/dist/routes/github-callback.js +129 -0
  148. package/dist/routes/github-callback.js.map +1 -0
  149. package/dist/routes/github.d.ts +6 -0
  150. package/dist/routes/github.d.ts.map +1 -0
  151. package/dist/routes/github.js +7 -0
  152. package/dist/routes/github.js.map +1 -0
  153. package/dist/routes/integrations.d.ts +6 -0
  154. package/dist/routes/integrations.d.ts.map +1 -0
  155. package/dist/routes/integrations.js +7 -0
  156. package/dist/routes/integrations.js.map +1 -0
  157. package/dist/routes/linear-callback.d.ts +6 -0
  158. package/dist/routes/linear-callback.d.ts.map +1 -0
  159. package/dist/routes/linear-callback.js +7 -0
  160. package/dist/routes/linear-callback.js.map +1 -0
  161. package/dist/routes/linear.d.ts +6 -0
  162. package/dist/routes/linear.d.ts.map +1 -0
  163. package/dist/routes/linear.js +7 -0
  164. package/dist/routes/linear.js.map +1 -0
  165. package/dist/routes/sentry-callback.d.ts +6 -0
  166. package/dist/routes/sentry-callback.d.ts.map +1 -0
  167. package/dist/routes/sentry-callback.js +7 -0
  168. package/dist/routes/sentry-callback.js.map +1 -0
  169. package/dist/routes/sentry.d.ts +6 -0
  170. package/dist/routes/sentry.d.ts.map +1 -0
  171. package/dist/routes/sentry.js +7 -0
  172. package/dist/routes/sentry.js.map +1 -0
  173. package/dist/sentry-callback.d.ts +35 -0
  174. package/dist/sentry-callback.d.ts.map +1 -0
  175. package/dist/sentry-callback.js +132 -0
  176. package/dist/sentry-callback.js.map +1 -0
  177. package/dist/tsconfig.test.tsbuildinfo +1 -0
  178. package/package.json +120 -0
  179. package/src/components/connection-picker.tsx +89 -0
  180. package/src/components/installed-integrations-section.tsx +198 -0
  181. package/src/components/integration-delete-confirm-modal.tsx +62 -0
  182. package/src/components/integration-gallery-for-workspace.tsx +193 -0
  183. package/src/components/integration-gallery.stories.tsx +302 -0
  184. package/src/components/integration-gallery.test.tsx +692 -0
  185. package/src/components/integration-gallery.tsx +49 -0
  186. package/src/components/integration-usage-events.test.ts +80 -0
  187. package/src/components/integration-usage-events.ts +116 -0
  188. package/src/components/integration-usage-modal.tsx +178 -0
  189. package/src/components/provider-grid.tsx +163 -0
  190. package/src/components/redirect-install-page.test.tsx +98 -0
  191. package/src/components/redirect-install-page.tsx +68 -0
  192. package/src/components/repository-picker.tsx +102 -0
  193. package/src/components/webhook/copyable-value.test.tsx +41 -0
  194. package/src/components/webhook/copyable-value.tsx +72 -0
  195. package/src/components/webhook/webhook-create-modal.tsx +247 -0
  196. package/src/components/webhook/webhook-form-errors.test.ts +45 -0
  197. package/src/components/webhook/webhook-form-errors.ts +32 -0
  198. package/src/components/webhook/webhook-modals.stories.tsx +225 -0
  199. package/src/components/webhook/webhook-public-endpoint-alert.tsx +15 -0
  200. package/src/components/webhook/webhook-usage-details.tsx +44 -0
  201. package/src/connection-status-badge.tsx +38 -0
  202. package/src/feature.ts +47 -0
  203. package/src/hooks/api/integrations.test.ts +105 -0
  204. package/src/hooks/api/integrations.ts +287 -0
  205. package/src/hooks/api/webhook-connections.ts +115 -0
  206. package/src/index.ts +16 -0
  207. package/src/integration-icon.react.test.tsx +40 -0
  208. package/src/integration-icon.test.ts +19 -0
  209. package/src/integration-icon.tsx +26 -0
  210. package/src/linear-callback.test.ts +227 -0
  211. package/src/linear-callback.ts +136 -0
  212. package/src/pages/gitea-form-errors.test.ts +55 -0
  213. package/src/pages/gitea-form-errors.ts +36 -0
  214. package/src/pages/gitea-install-page.tsx +108 -0
  215. package/src/pages/github-install-page.tsx +11 -0
  216. package/src/pages/linear-callback-page.test.tsx +157 -0
  217. package/src/pages/linear-callback-page.tsx +175 -0
  218. package/src/pages/linear-install-page.tsx +26 -0
  219. package/src/pages/sentry-callback-page.test.tsx +205 -0
  220. package/src/pages/sentry-callback-page.tsx +238 -0
  221. package/src/pages/sentry-install-page.tsx +17 -0
  222. package/src/pages/source-control-onboarding-page.test.tsx +46 -0
  223. package/src/pages/source-control-onboarding-page.tsx +26 -0
  224. package/src/provider-catalog.test.ts +42 -0
  225. package/src/provider-catalog.ts +49 -0
  226. package/src/routes/gitea.tsx +4 -0
  227. package/src/routes/github-callback.tsx +114 -0
  228. package/src/routes/github.tsx +4 -0
  229. package/src/routes/integrations.tsx +4 -0
  230. package/src/routes/linear-callback.tsx +4 -0
  231. package/src/routes/linear.tsx +4 -0
  232. package/src/routes/sentry-callback.tsx +4 -0
  233. package/src/routes/sentry.tsx +4 -0
  234. package/src/sentry-callback.test.ts +239 -0
  235. package/src/sentry-callback.ts +142 -0
  236. package/test/render.tsx +111 -0
  237. package/test/setup.ts +3 -0
  238. package/tsconfig.build.json +9 -0
  239. package/tsconfig.build.tsbuildinfo +1 -0
  240. package/tsconfig.json +3 -0
  241. package/tsconfig.test.json +8 -0
  242. package/vercel.json +8 -0
  243. package/vitest.config.ts +75 -0
@@ -0,0 +1,192 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRefreshAuth } from '@shipfox/client-auth';
3
+ import { ButtonLink } from '@shipfox/react-ui/button';
4
+ import { Callout } from '@shipfox/react-ui/callout';
5
+ import { FullPageLoader } from '@shipfox/react-ui/loader';
6
+ import { toast } from '@shipfox/react-ui/toast';
7
+ import { Text } from '@shipfox/react-ui/typography';
8
+ import { useQueryClient } from '@tanstack/react-query';
9
+ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
10
+ import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { integrationsQueryKeys, useCompleteLinearCallbackMutation } from '#hooks/api/integrations.js';
12
+ import { classifyLinearCallbackError, clearLinearInstallWorkspace, parseLinearCallbackQuery, readLinearInstallWorkspace, serializeLinearCallbackQuery } from '#linear-callback.js';
13
+ // Keyed by the callback's code+state. This page has no in-place Retry (failures
14
+ // route to "Start over", which begins a fresh install with a new state+code), so
15
+ // entries are intentionally never evicted: retaining them dedupes StrictMode and
16
+ // remount re-runs so the single-use grant code is exchanged at most once. The
17
+ // sibling Sentry page evicts on settle only because its Retry replays the code.
18
+ const callbackRequests = new Map();
19
+ // Keeps the success toast firing once per distinct callback even though the
20
+ // effect re-runs against the cached request as the mutation identity churns.
21
+ const toastedCallbacks = new Set();
22
+ export function LinearCallbackPage() {
23
+ const search = useSearch({
24
+ strict: false
25
+ });
26
+ const navigate = useNavigate();
27
+ const refreshAuth = useRefreshAuth();
28
+ const queryClient = useQueryClient();
29
+ const { mutateAsync: completeLinearCallback } = useCompleteLinearCallbackMutation();
30
+ const params = useMemo(()=>parseLinearCallbackQuery(search), [
31
+ search
32
+ ]);
33
+ const workspaceId = useMemo(()=>{
34
+ try {
35
+ return readLinearInstallWorkspace(window.sessionStorage);
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }, []);
40
+ const [failure, setFailure] = useState();
41
+ useEffect(()=>{
42
+ if (!params) return;
43
+ let disposed = false;
44
+ const key = serializeLinearCallbackQuery(params);
45
+ let request = callbackRequests.get(key);
46
+ if (!request) {
47
+ request = refreshAuth().then(async (session)=>await completeLinearCallback({
48
+ query: params,
49
+ token: session.token
50
+ }));
51
+ callbackRequests.set(key, request);
52
+ }
53
+ request.then(async (connection)=>{
54
+ if (disposed) return;
55
+ try {
56
+ clearLinearInstallWorkspace(window.sessionStorage);
57
+ } catch {
58
+ // The successful API response remains the source of truth for navigation.
59
+ }
60
+ try {
61
+ await queryClient.invalidateQueries({
62
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(connection.workspace_id)
63
+ });
64
+ } catch {
65
+ // Cache refresh is best effort: the successful callback is already committed server-side.
66
+ }
67
+ if (disposed) return;
68
+ if (!toastedCallbacks.has(key)) {
69
+ toastedCallbacks.add(key);
70
+ toast.success('Linear installed.');
71
+ }
72
+ try {
73
+ await navigate({
74
+ to: '/workspaces/$wid/settings/integrations',
75
+ params: {
76
+ wid: connection.workspace_id
77
+ },
78
+ replace: true
79
+ });
80
+ } catch {
81
+ // Keep the completed callback page visible if client navigation is interrupted.
82
+ }
83
+ }, (error)=>{
84
+ if (disposed) return;
85
+ setFailure(classifyLinearCallbackError(error));
86
+ });
87
+ return ()=>{
88
+ disposed = true;
89
+ };
90
+ }, [
91
+ completeLinearCallback,
92
+ navigate,
93
+ params,
94
+ queryClient,
95
+ refreshAuth
96
+ ]);
97
+ if (!params) {
98
+ return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
99
+ failure: {
100
+ title: 'Invalid Linear callback',
101
+ message: 'Invalid Linear callback. Start the install again from workspace settings.',
102
+ startOver: true,
103
+ signIn: false
104
+ },
105
+ workspaceId: workspaceId
106
+ });
107
+ }
108
+ if (!failure) return /*#__PURE__*/ _jsx(FullPageLoader, {
109
+ "aria-label": "Connecting Linear"
110
+ });
111
+ return /*#__PURE__*/ _jsx(LinearCallbackFailurePage, {
112
+ failure: failure,
113
+ workspaceId: workspaceId
114
+ });
115
+ }
116
+ function LinearCallbackFailurePage({ failure, workspaceId }) {
117
+ const headingRef = useRef(null);
118
+ useEffect(()=>{
119
+ headingRef.current?.focus();
120
+ }, []);
121
+ const recoveryVariant = failure.startOver || failure.signIn ? 'muted' : 'base';
122
+ const switchAccountHref = workspaceId ? `/auth/logout?redirect=${encodeURIComponent(`/workspaces/${workspaceId}/integrations/linear`)}` : '/auth/logout';
123
+ const settingsLink = workspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
124
+ asChild: true,
125
+ variant: recoveryVariant,
126
+ className: "min-h-44 w-full sm:w-fit",
127
+ children: /*#__PURE__*/ _jsx(Link, {
128
+ to: "/workspaces/$wid/settings/integrations",
129
+ params: {
130
+ wid: workspaceId
131
+ },
132
+ children: "Back to integrations"
133
+ })
134
+ }) : /*#__PURE__*/ _jsx(ButtonLink, {
135
+ asChild: true,
136
+ variant: recoveryVariant,
137
+ className: "min-h-44 w-full sm:w-fit",
138
+ children: /*#__PURE__*/ _jsx(Link, {
139
+ to: "/",
140
+ children: "Back to Shipfox"
141
+ })
142
+ });
143
+ return /*#__PURE__*/ _jsx("main", {
144
+ className: "flex min-h-screen bg-background-subtle-base px-16 py-32",
145
+ children: /*#__PURE__*/ _jsxs("div", {
146
+ className: "mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20",
147
+ children: [
148
+ /*#__PURE__*/ _jsx("h2", {
149
+ ref: headingRef,
150
+ tabIndex: -1,
151
+ className: "text-24 font-semibold outline-none",
152
+ children: failure.title ?? 'Linear install could not be completed'
153
+ }),
154
+ /*#__PURE__*/ _jsx(Callout, {
155
+ role: "alert",
156
+ type: "error",
157
+ children: /*#__PURE__*/ _jsx(Text, {
158
+ size: "sm",
159
+ children: failure.message
160
+ })
161
+ }),
162
+ /*#__PURE__*/ _jsxs("div", {
163
+ className: "flex flex-col gap-8 sm:flex-row sm:items-center",
164
+ children: [
165
+ failure.signIn ? /*#__PURE__*/ _jsx(ButtonLink, {
166
+ asChild: true,
167
+ className: "min-h-44 w-full sm:w-fit",
168
+ children: /*#__PURE__*/ _jsx(Link, {
169
+ to: switchAccountHref,
170
+ children: "Switch account"
171
+ })
172
+ }) : null,
173
+ failure.startOver && workspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
174
+ asChild: true,
175
+ className: "min-h-44 w-full sm:w-fit",
176
+ children: /*#__PURE__*/ _jsx(Link, {
177
+ to: "/workspaces/$wid/integrations/linear",
178
+ params: {
179
+ wid: workspaceId
180
+ },
181
+ children: "Start over"
182
+ })
183
+ }) : null,
184
+ settingsLink
185
+ ]
186
+ })
187
+ ]
188
+ })
189
+ });
190
+ }
191
+
192
+ //# sourceMappingURL=linear-callback-page.js.map
@@ -0,0 +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 {ButtonLink} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {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 {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// Keyed by the callback's code+state. This page has no in-place Retry (failures\n// route to \"Start over\", which begins a fresh install with a new state+code), so\n// entries are intentionally never evicted: retaining them dedupes StrictMode and\n// remount re-runs so the single-use grant code is exchanged at most once. The\n// sibling Sentry page evicts on settle only because its Retry replays the code.\nconst callbackRequests = new Map<string, Promise<LinearCallbackResponseDto>>();\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 let request = callbackRequests.get(key);\n if (!request) {\n request = refreshAuth().then(\n async (session) => await completeLinearCallback({query: params, token: session.token}),\n );\n callbackRequests.set(key, request);\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 const headingRef = useRef<HTMLHeadingElement>(null);\n\n useEffect(() => {\n headingRef.current?.focus();\n }, []);\n\n const recoveryVariant = failure.startOver || failure.signIn ? 'muted' : 'base';\n const switchAccountHref = workspaceId\n ? `/auth/logout?redirect=${encodeURIComponent(`/workspaces/${workspaceId}/integrations/linear`)}`\n : '/auth/logout';\n const settingsLink = workspaceId ? (\n <ButtonLink asChild variant={recoveryVariant} className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/workspaces/$wid/settings/integrations\" params={{wid: workspaceId}}>\n Back to integrations\n </Link>\n </ButtonLink>\n ) : (\n <ButtonLink asChild variant={recoveryVariant} className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/\">Back to Shipfox</Link>\n </ButtonLink>\n );\n\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 <h2 ref={headingRef} tabIndex={-1} className=\"text-24 font-semibold outline-none\">\n {failure.title ?? 'Linear install could not be completed'}\n </h2>\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">{failure.message}</Text>\n </Callout>\n <div className=\"flex flex-col gap-8 sm:flex-row sm:items-center\">\n {failure.signIn ? (\n <ButtonLink asChild className=\"min-h-44 w-full sm:w-fit\">\n <Link to={switchAccountHref}>Switch account</Link>\n </ButtonLink>\n ) : null}\n {failure.startOver && workspaceId ? (\n <ButtonLink asChild className=\"min-h-44 w-full sm:w-fit\">\n <Link to=\"/workspaces/$wid/integrations/linear\" params={{wid: workspaceId}}>\n Start over\n </Link>\n </ButtonLink>\n ) : null}\n {settingsLink}\n </div>\n </div>\n </main>\n );\n}\n"],"names":["useRefreshAuth","ButtonLink","Callout","FullPageLoader","toast","Text","useQueryClient","Link","useNavigate","useSearch","useEffect","useMemo","useRef","useState","integrationsQueryKeys","useCompleteLinearCallbackMutation","classifyLinearCallbackError","clearLinearInstallWorkspace","parseLinearCallbackQuery","readLinearInstallWorkspace","serializeLinearCallbackQuery","callbackRequests","Map","toastedCallbacks","Set","LinearCallbackPage","search","strict","navigate","refreshAuth","queryClient","mutateAsync","completeLinearCallback","params","workspaceId","window","sessionStorage","undefined","failure","setFailure","disposed","key","request","get","then","session","query","token","set","connection","invalidateQueries","queryKey","connectionsByWorkspace","workspace_id","has","add","success","to","wid","replace","error","LinearCallbackFailurePage","title","message","startOver","signIn","aria-label","headingRef","current","focus","recoveryVariant","switchAccountHref","encodeURIComponent","settingsLink","asChild","variant","className","main","div","h2","ref","tabIndex","role","type","size"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function LinearInstallPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=linear-install-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linear-install-page.d.ts","sourceRoot":"","sources":["../../src/pages/linear-install-page.tsx"],"names":[],"mappings":"AAKA,wBAAgB,iBAAiB,gCAoBhC"}
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useCallback } from 'react';
3
+ import { RedirectInstallPage } from '#components/redirect-install-page.js';
4
+ import { useCreateLinearInstallMutation } from '#hooks/api/integrations.js';
5
+ import { saveLinearInstallWorkspace } from '#linear-callback.js';
6
+ export function LinearInstallPage() {
7
+ const createInstall = useCreateLinearInstallMutation();
8
+ const installRequest = useCallback(async (body)=>await createInstall.mutateAsync(body), [
9
+ createInstall
10
+ ]);
11
+ return /*#__PURE__*/ _jsx(RedirectInstallPage, {
12
+ installRequest: installRequest,
13
+ errorFallbackMessage: "Could not start Linear install.",
14
+ beforeRedirect: (workspaceId)=>{
15
+ try {
16
+ saveLinearInstallWorkspace(window.sessionStorage, workspaceId);
17
+ } catch {
18
+ // Storage can be disabled before the helper gets a usable Storage object.
19
+ }
20
+ }
21
+ });
22
+ }
23
+
24
+ //# sourceMappingURL=linear-install-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/linear-install-page.tsx"],"sourcesContent":["import {useCallback} from 'react';\nimport {RedirectInstallPage} from '#components/redirect-install-page.js';\nimport {useCreateLinearInstallMutation} from '#hooks/api/integrations.js';\nimport {saveLinearInstallWorkspace} from '#linear-callback.js';\n\nexport function LinearInstallPage() {\n const createInstall = useCreateLinearInstallMutation();\n const installRequest = useCallback(\n async (body: {workspace_id: string}) => await createInstall.mutateAsync(body),\n [createInstall],\n );\n\n return (\n <RedirectInstallPage\n installRequest={installRequest}\n errorFallbackMessage=\"Could not start Linear install.\"\n beforeRedirect={(workspaceId) => {\n try {\n saveLinearInstallWorkspace(window.sessionStorage, workspaceId);\n } catch {\n // Storage can be disabled before the helper gets a usable Storage object.\n }\n }}\n />\n );\n}\n"],"names":["useCallback","RedirectInstallPage","useCreateLinearInstallMutation","saveLinearInstallWorkspace","LinearInstallPage","createInstall","installRequest","body","mutateAsync","errorFallbackMessage","beforeRedirect","workspaceId","window","sessionStorage"],"mappings":";AAAA,SAAQA,WAAW,QAAO,QAAQ;AAClC,SAAQC,mBAAmB,QAAO,uCAAuC;AACzE,SAAQC,8BAA8B,QAAO,6BAA6B;AAC1E,SAAQC,0BAA0B,QAAO,sBAAsB;AAE/D,OAAO,SAASC;IACd,MAAMC,gBAAgBH;IACtB,MAAMI,iBAAiBN,YACrB,OAAOO,OAAiC,MAAMF,cAAcG,WAAW,CAACD,OACxE;QAACF;KAAc;IAGjB,qBACE,KAACJ;QACCK,gBAAgBA;QAChBG,sBAAqB;QACrBC,gBAAgB,CAACC;YACf,IAAI;gBACFR,2BAA2BS,OAAOC,cAAc,EAAEF;YACpD,EAAE,OAAM;YACN,0EAA0E;YAC5E;QACF;;AAGN"}
@@ -0,0 +1,2 @@
1
+ export declare function SentryCallbackPage(): import("react").JSX.Element | null;
2
+ //# sourceMappingURL=sentry-callback-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-callback-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-callback-page.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,kBAAkB,uCAuMjC"}
@@ -0,0 +1,256 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAuthState, useRefreshAuth } from '@shipfox/client-auth';
3
+ import { Button, ButtonLink } from '@shipfox/react-ui/button';
4
+ import { Callout } from '@shipfox/react-ui/callout';
5
+ import { Card } from '@shipfox/react-ui/card';
6
+ import { FullPageLoader } from '@shipfox/react-ui/loader';
7
+ import { toast } from '@shipfox/react-ui/toast';
8
+ import { Header, Text } from '@shipfox/react-ui/typography';
9
+ import { useQueryClient } from '@tanstack/react-query';
10
+ import { Link, useNavigate, useSearch } from '@tanstack/react-router';
11
+ import { useEffect, useMemo, useRef, useState } from 'react';
12
+ import { connectSentry, integrationsQueryKeys } from '#hooks/api/integrations.js';
13
+ import { classifySentryConnectError, clearSentryInstallWorkspace, parseSentryCallbackParams, preselectSentryWorkspace, readSentryInstallWorkspace } from '#sentry-callback.js';
14
+ // De-dupes concurrent attempts for the same installation+workspace+code
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();
20
+ export function SentryCallbackPage() {
21
+ const search = useSearch({
22
+ strict: false
23
+ });
24
+ const navigate = useNavigate();
25
+ const refreshAuth = useRefreshAuth();
26
+ const { workspaces, isLoading } = useAuthState();
27
+ const queryClient = useQueryClient();
28
+ const params = useMemo(()=>parseSentryCallbackParams(search), [
29
+ search
30
+ ]);
31
+ const storedWorkspaceId = useMemo(()=>readSentryInstallWorkspace(window.sessionStorage), []);
32
+ const preselection = preselectSentryWorkspace(storedWorkspaceId, workspaces);
33
+ const preselectedId = preselection.kind === 'pick' ? preselection.preselectedId : undefined;
34
+ const [connectingId, setConnectingId] = useState();
35
+ const [failure, setFailure] = useState();
36
+ const [retryLocked, setRetryLocked] = useState(false);
37
+ const disposedRef = useRef(false);
38
+ useEffect(()=>{
39
+ disposedRef.current = false;
40
+ return ()=>{
41
+ disposedRef.current = true;
42
+ };
43
+ }, []);
44
+ // Wait for auth before treating an empty workspace list as "no workspace": on
45
+ // a cold return from Sentry, `workspaces` is [] until the session refresh
46
+ // resolves, and bouncing to `/` then would discard the one-shot grant code.
47
+ const noWorkspace = !isLoading && preselection.kind === 'none';
48
+ useEffect(()=>{
49
+ if (!noWorkspace) return;
50
+ toast.error('You need a workspace before installing Sentry.');
51
+ navigate({
52
+ to: '/',
53
+ replace: true
54
+ });
55
+ }, [
56
+ noWorkspace,
57
+ navigate
58
+ ]);
59
+ const retryAfterSeconds = failure?.failure.kind === 'retryable' ? failure.failure.retryAfterSeconds : undefined;
60
+ useEffect(()=>{
61
+ // No backoff window (any non-rate-limited failure) re-enables Retry, so a
62
+ // prior rate-limit lock can never strand the button in a disabled state.
63
+ if (!retryAfterSeconds) {
64
+ setRetryLocked(false);
65
+ return;
66
+ }
67
+ setRetryLocked(true);
68
+ const timer = setTimeout(()=>setRetryLocked(false), retryAfterSeconds * 1000);
69
+ return ()=>clearTimeout(timer);
70
+ }, [
71
+ retryAfterSeconds
72
+ ]);
73
+ if (!params) {
74
+ return /*#__PURE__*/ _jsxs(CallbackColumn, {
75
+ children: [
76
+ /*#__PURE__*/ _jsx(Callout, {
77
+ role: "alert",
78
+ type: "error",
79
+ children: /*#__PURE__*/ _jsx(Text, {
80
+ size: "sm",
81
+ children: "This Sentry link is missing required parameters. Start the install again from your workspace settings."
82
+ })
83
+ }),
84
+ /*#__PURE__*/ _jsx(ButtonLink, {
85
+ asChild: true,
86
+ variant: "muted",
87
+ className: "w-fit",
88
+ children: /*#__PURE__*/ _jsx(Link, {
89
+ to: "/",
90
+ children: "Back to Shipfox"
91
+ })
92
+ })
93
+ ]
94
+ });
95
+ }
96
+ // Hold the loader until auth resolves; the workspace list and the
97
+ // noWorkspace redirect both depend on a settled `workspaces`.
98
+ if (isLoading) return /*#__PURE__*/ _jsx(FullPageLoader, {});
99
+ if (noWorkspace) return null;
100
+ const connect = (workspaceId)=>{
101
+ setConnectingId(workspaceId);
102
+ setFailure(undefined);
103
+ const key = `${params.installationId}|${workspaceId}|${params.code}`;
104
+ let request = connectRequests.get(key);
105
+ if (!request) {
106
+ request = refreshAuth().then((session)=>connectSentry({
107
+ body: {
108
+ workspace_id: workspaceId,
109
+ code: params.code,
110
+ installation_id: params.installationId
111
+ },
112
+ 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
+ }
122
+ request.then(async ()=>{
123
+ clearSentryInstallWorkspace(window.sessionStorage);
124
+ await queryClient.invalidateQueries({
125
+ queryKey: integrationsQueryKeys.connectionsByWorkspace(workspaceId)
126
+ });
127
+ if (disposedRef.current) return;
128
+ toast.success('Sentry installed.');
129
+ await navigate({
130
+ to: '/workspaces/$wid/settings/integrations',
131
+ params: {
132
+ wid: workspaceId
133
+ },
134
+ replace: true
135
+ });
136
+ }).catch((error)=>{
137
+ if (disposedRef.current) return;
138
+ const classified = classifySentryConnectError(error);
139
+ if (classified.kind === 'terminal') {
140
+ // Only a fresh install (new grant code) can recover; the stored
141
+ // handoff has served its purpose either way.
142
+ clearSentryInstallWorkspace(window.sessionStorage);
143
+ }
144
+ setConnectingId(undefined);
145
+ setFailure({
146
+ workspaceId,
147
+ failure: classified
148
+ });
149
+ });
150
+ };
151
+ const orderedWorkspaces = preselectedId ? [
152
+ ...workspaces.filter((workspace)=>workspace.id === preselectedId),
153
+ ...workspaces.filter((workspace)=>workspace.id !== preselectedId)
154
+ ] : workspaces;
155
+ const failureWorkspaceId = failure?.workspaceId ?? preselectedId ?? workspaces[0]?.id;
156
+ return /*#__PURE__*/ _jsxs(CallbackColumn, {
157
+ children: [
158
+ /*#__PURE__*/ _jsxs("header", {
159
+ className: "flex flex-col gap-8",
160
+ children: [
161
+ /*#__PURE__*/ _jsx(Header, {
162
+ variant: "h2",
163
+ children: "Install Sentry"
164
+ }),
165
+ /*#__PURE__*/ _jsx(Text, {
166
+ size: "sm",
167
+ className: "text-foreground-neutral-muted",
168
+ children: params.orgSlug ? `Install the Sentry org "${params.orgSlug}" in a workspace.` : 'Install this Sentry integration in a workspace.'
169
+ })
170
+ ]
171
+ }),
172
+ failure ? /*#__PURE__*/ _jsx(Callout, {
173
+ role: "alert",
174
+ type: "error",
175
+ children: /*#__PURE__*/ _jsxs("div", {
176
+ className: "flex flex-col gap-8",
177
+ children: [
178
+ /*#__PURE__*/ _jsx(Text, {
179
+ size: "sm",
180
+ children: failure.failure.message
181
+ }),
182
+ failure.failure.kind === 'retryable' ? /*#__PURE__*/ _jsx(Button, {
183
+ size: "sm",
184
+ variant: "secondary",
185
+ className: "w-fit",
186
+ disabled: retryLocked || connectingId !== undefined,
187
+ onClick: ()=>connect(failure.workspaceId),
188
+ children: "Retry"
189
+ }) : null,
190
+ failure.failure.kind === 'terminal' && failure.failure.startOver ? /*#__PURE__*/ _jsx(Button, {
191
+ asChild: true,
192
+ size: "sm",
193
+ variant: "secondary",
194
+ className: "w-fit",
195
+ children: /*#__PURE__*/ _jsx(Link, {
196
+ to: "/workspaces/$wid/integrations/sentry",
197
+ params: {
198
+ wid: failure.workspaceId
199
+ },
200
+ children: "Start over"
201
+ })
202
+ }) : null
203
+ ]
204
+ })
205
+ }) : null,
206
+ /*#__PURE__*/ _jsx("section", {
207
+ className: "flex flex-col gap-8",
208
+ "aria-label": "Choose a workspace",
209
+ children: orderedWorkspaces.map((workspace)=>/*#__PURE__*/ _jsx(Card, {
210
+ className: "p-16",
211
+ children: /*#__PURE__*/ _jsxs("div", {
212
+ className: "flex items-center justify-between gap-12",
213
+ children: [
214
+ /*#__PURE__*/ _jsx(Text, {
215
+ size: "md",
216
+ bold: true,
217
+ className: "truncate",
218
+ children: workspace.name
219
+ }),
220
+ /*#__PURE__*/ _jsx(Button, {
221
+ variant: "secondary",
222
+ disabled: connectingId !== undefined,
223
+ isLoading: connectingId === workspace.id,
224
+ onClick: ()=>connect(workspace.id),
225
+ children: "Install"
226
+ })
227
+ ]
228
+ })
229
+ }, workspace.id))
230
+ }),
231
+ failureWorkspaceId ? /*#__PURE__*/ _jsx(ButtonLink, {
232
+ asChild: true,
233
+ variant: "muted",
234
+ className: "w-fit",
235
+ children: /*#__PURE__*/ _jsx(Link, {
236
+ to: "/workspaces/$wid/settings/integrations",
237
+ params: {
238
+ wid: failureWorkspaceId
239
+ },
240
+ children: "Back to settings"
241
+ })
242
+ }) : null
243
+ ]
244
+ });
245
+ }
246
+ function CallbackColumn({ children }) {
247
+ return /*#__PURE__*/ _jsx("main", {
248
+ className: "flex min-h-screen bg-background-subtle-base px-16 py-32",
249
+ children: /*#__PURE__*/ _jsx("div", {
250
+ className: "mx-auto flex w-full max-w-[480px] flex-col justify-center gap-20",
251
+ children: children
252
+ })
253
+ });
254
+ }
255
+
256
+ //# sourceMappingURL=sentry-callback-page.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,2 @@
1
+ export declare function SentryInstallPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=sentry-install-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sentry-install-page.d.ts","sourceRoot":"","sources":["../../src/pages/sentry-install-page.tsx"],"names":[],"mappings":"AAIA,wBAAgB,iBAAiB,gCAYhC"}
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RedirectInstallPage } from '#components/redirect-install-page.js';
3
+ import { createSentryInstall } from '#hooks/api/integrations.js';
4
+ import { saveSentryInstallWorkspace } from '#sentry-callback.js';
5
+ export function SentryInstallPage() {
6
+ return /*#__PURE__*/ _jsx(RedirectInstallPage, {
7
+ installRequest: createSentryInstall,
8
+ errorFallbackMessage: "Could not start Sentry install.",
9
+ // Sentry's redirect has no state param; the stored id lets the callback
10
+ // pre-select this workspace. Saving never throws (see helper).
11
+ beforeRedirect: (workspaceId)=>{
12
+ saveSentryInstallWorkspace(window.sessionStorage, workspaceId);
13
+ }
14
+ });
15
+ }
16
+
17
+ //# sourceMappingURL=sentry-install-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/sentry-install-page.tsx"],"sourcesContent":["import {RedirectInstallPage} from '#components/redirect-install-page.js';\nimport {createSentryInstall} from '#hooks/api/integrations.js';\nimport {saveSentryInstallWorkspace} from '#sentry-callback.js';\n\nexport function SentryInstallPage() {\n return (\n <RedirectInstallPage\n installRequest={createSentryInstall}\n errorFallbackMessage=\"Could not start Sentry install.\"\n // Sentry's redirect has no state param; the stored id lets the callback\n // pre-select this workspace. Saving never throws (see helper).\n beforeRedirect={(workspaceId) => {\n saveSentryInstallWorkspace(window.sessionStorage, workspaceId);\n }}\n />\n );\n}\n"],"names":["RedirectInstallPage","createSentryInstall","saveSentryInstallWorkspace","SentryInstallPage","installRequest","errorFallbackMessage","beforeRedirect","workspaceId","window","sessionStorage"],"mappings":";AAAA,SAAQA,mBAAmB,QAAO,uCAAuC;AACzE,SAAQC,mBAAmB,QAAO,6BAA6B;AAC/D,SAAQC,0BAA0B,QAAO,sBAAsB;AAE/D,OAAO,SAASC;IACd,qBACE,KAACH;QACCI,gBAAgBH;QAChBI,sBAAqB;QACrB,wEAAwE;QACxE,+DAA+D;QAC/DC,gBAAgB,CAACC;YACfL,2BAA2BM,OAAOC,cAAc,EAAEF;QACpD;;AAGN"}
@@ -0,0 +1,2 @@
1
+ export declare function SourceControlOnboardingPage(): import("react").JSX.Element;
2
+ //# sourceMappingURL=source-control-onboarding-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-control-onboarding-page.d.ts","sourceRoot":"","sources":["../../src/pages/source-control-onboarding-page.tsx"],"names":[],"mappings":"AAKA,wBAAgB,2BAA2B,gCAoB1C"}
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useActiveWorkspace } from '@shipfox/client-auth';
3
+ import { Header, Text } from '@shipfox/react-ui/typography';
4
+ import { ProviderGrid } from '#components/provider-grid.js';
5
+ import { useIntegrationProvidersQuery } from '#hooks/api/integrations.js';
6
+ export function SourceControlOnboardingPage() {
7
+ const workspace = useActiveWorkspace();
8
+ const providersQuery = useIntegrationProvidersQuery({
9
+ capability: 'source_control'
10
+ });
11
+ return /*#__PURE__*/ _jsxs("div", {
12
+ className: "mx-auto flex w-full max-w-[640px] flex-col gap-20",
13
+ children: [
14
+ /*#__PURE__*/ _jsxs("header", {
15
+ className: "flex flex-col gap-8",
16
+ children: [
17
+ /*#__PURE__*/ _jsx(Header, {
18
+ variant: "h1",
19
+ children: "Install source control"
20
+ }),
21
+ /*#__PURE__*/ _jsx(Text, {
22
+ size: "md",
23
+ className: "text-foreground-neutral-muted",
24
+ children: "Shipfox needs a source control integration to import your repositories."
25
+ })
26
+ ]
27
+ }),
28
+ /*#__PURE__*/ _jsx(ProviderGrid, {
29
+ providersQuery: providersQuery,
30
+ workspaceId: workspace.id,
31
+ emptyMessage: "Enable at least one source-control provider in the application settings."
32
+ })
33
+ ]
34
+ });
35
+ }
36
+
37
+ //# sourceMappingURL=source-control-onboarding-page.js.map