@shipfox/client-integrations 4.0.0 → 6.0.1

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 (194) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +86 -0
  3. package/dist/application/complete-integration-callback.d.ts +13 -0
  4. package/dist/application/complete-integration-callback.d.ts.map +1 -0
  5. package/dist/application/complete-integration-callback.js +26 -0
  6. package/dist/application/complete-integration-callback.js.map +1 -0
  7. package/dist/components/connection-picker.d.ts +2 -2
  8. package/dist/components/connection-picker.d.ts.map +1 -1
  9. package/dist/components/connection-picker.js +4 -4
  10. package/dist/components/connection-picker.js.map +1 -1
  11. package/dist/components/installed-integrations-section.d.ts +8 -6
  12. package/dist/components/installed-integrations-section.d.ts.map +1 -1
  13. package/dist/components/installed-integrations-section.js +22 -15
  14. package/dist/components/installed-integrations-section.js.map +1 -1
  15. package/dist/components/integration-gallery-for-workspace.d.ts +2 -2
  16. package/dist/components/integration-gallery-for-workspace.d.ts.map +1 -1
  17. package/dist/components/integration-gallery-for-workspace.js +14 -7
  18. package/dist/components/integration-gallery-for-workspace.js.map +1 -1
  19. package/dist/components/integration-gallery.d.ts +2 -2
  20. package/dist/components/integration-gallery.d.ts.map +1 -1
  21. package/dist/components/integration-gallery.js.map +1 -1
  22. package/dist/components/integration-usage-events.d.ts +1 -3
  23. package/dist/components/integration-usage-events.d.ts.map +1 -1
  24. package/dist/components/integration-usage-events.js +1 -125
  25. package/dist/components/integration-usage-events.js.map +1 -1
  26. package/dist/components/integration-usage-modal.d.ts +2 -2
  27. package/dist/components/integration-usage-modal.d.ts.map +1 -1
  28. package/dist/components/integration-usage-modal.js +2 -2
  29. package/dist/components/integration-usage-modal.js.map +1 -1
  30. package/dist/components/provider-grid.d.ts +7 -4
  31. package/dist/components/provider-grid.d.ts.map +1 -1
  32. package/dist/components/provider-grid.js +15 -10
  33. package/dist/components/provider-grid.js.map +1 -1
  34. package/dist/components/redirect-install-page.d.ts +2 -3
  35. package/dist/components/redirect-install-page.d.ts.map +1 -1
  36. package/dist/components/redirect-install-page.js +1 -1
  37. package/dist/components/redirect-install-page.js.map +1 -1
  38. package/dist/components/repository-picker.d.ts +2 -2
  39. package/dist/components/repository-picker.d.ts.map +1 -1
  40. package/dist/components/repository-picker.js +4 -4
  41. package/dist/components/repository-picker.js.map +1 -1
  42. package/dist/components/webhook/webhook-create-modal.d.ts +2 -2
  43. package/dist/components/webhook/webhook-create-modal.d.ts.map +1 -1
  44. package/dist/components/webhook/webhook-create-modal.js +8 -8
  45. package/dist/components/webhook/webhook-create-modal.js.map +1 -1
  46. package/dist/components/webhook/webhook-modals.stories.d.ts.map +1 -1
  47. package/dist/components/webhook/webhook-modals.stories.js +7 -7
  48. package/dist/components/webhook/webhook-modals.stories.js.map +1 -1
  49. package/dist/components/webhook/webhook-usage-details.d.ts.map +1 -1
  50. package/dist/components/webhook/webhook-usage-details.js +2 -2
  51. package/dist/components/webhook/webhook-usage-details.js.map +1 -1
  52. package/dist/connection-status-badge.d.ts +2 -2
  53. package/dist/connection-status-badge.d.ts.map +1 -1
  54. package/dist/connection-status-badge.js +11 -15
  55. package/dist/connection-status-badge.js.map +1 -1
  56. package/dist/core/models.d.ts +66 -0
  57. package/dist/core/models.d.ts.map +1 -0
  58. package/dist/core/models.js +156 -0
  59. package/dist/core/models.js.map +1 -0
  60. package/dist/feature.d.ts +18 -0
  61. package/dist/feature.d.ts.map +1 -1
  62. package/dist/feature.js +16 -1
  63. package/dist/feature.js.map +1 -1
  64. package/dist/hooks/api/integration-mapper.d.ts +11 -0
  65. package/dist/hooks/api/integration-mapper.d.ts.map +1 -0
  66. package/dist/hooks/api/integration-mapper.js +54 -0
  67. package/dist/hooks/api/integration-mapper.js.map +1 -0
  68. package/dist/hooks/api/integrations.d.ts +47 -237
  69. package/dist/hooks/api/integrations.d.ts.map +1 -1
  70. package/dist/hooks/api/integrations.js +92 -42
  71. package/dist/hooks/api/integrations.js.map +1 -1
  72. package/dist/hooks/api/webhook-connections.d.ts +12 -64
  73. package/dist/hooks/api/webhook-connections.d.ts.map +1 -1
  74. package/dist/hooks/api/webhook-connections.js +17 -9
  75. package/dist/hooks/api/webhook-connections.js.map +1 -1
  76. package/dist/index.d.ts +2 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +2 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/linear-callback.d.ts +2 -1
  81. package/dist/linear-callback.d.ts.map +1 -1
  82. package/dist/linear-callback.js +14 -15
  83. package/dist/linear-callback.js.map +1 -1
  84. package/dist/pages/gitea-install-page.d.ts.map +1 -1
  85. package/dist/pages/gitea-install-page.js +1 -10
  86. package/dist/pages/gitea-install-page.js.map +1 -1
  87. package/dist/pages/linear-callback-page.d.ts.map +1 -1
  88. package/dist/pages/linear-callback-page.js +19 -32
  89. package/dist/pages/linear-callback-page.js.map +1 -1
  90. package/dist/pages/linear-install-page.d.ts.map +1 -1
  91. package/dist/pages/linear-install-page.js +2 -1
  92. package/dist/pages/linear-install-page.js.map +1 -1
  93. package/dist/pages/sentry-callback-page.d.ts.map +1 -1
  94. package/dist/pages/sentry-callback-page.js +22 -25
  95. package/dist/pages/sentry-callback-page.js.map +1 -1
  96. package/dist/pages/sentry-install-page.d.ts.map +1 -1
  97. package/dist/pages/sentry-install-page.js +2 -1
  98. package/dist/pages/sentry-install-page.js.map +1 -1
  99. package/dist/pages/slack-callback-page.d.ts.map +1 -1
  100. package/dist/pages/slack-callback-page.js +21 -28
  101. package/dist/pages/slack-callback-page.js.map +1 -1
  102. package/dist/pages/slack-install-page.d.ts.map +1 -1
  103. package/dist/pages/slack-install-page.js +2 -1
  104. package/dist/pages/slack-install-page.js.map +1 -1
  105. package/dist/pages/source-control-onboarding-page.d.ts.map +1 -1
  106. package/dist/pages/source-control-onboarding-page.js +5 -1
  107. package/dist/pages/source-control-onboarding-page.js.map +1 -1
  108. package/dist/provider-catalog.d.ts.map +1 -1
  109. package/dist/provider-catalog.js +7 -6
  110. package/dist/provider-catalog.js.map +1 -1
  111. package/dist/routes/github-callback.d.ts +16 -0
  112. package/dist/routes/github-callback.d.ts.map +1 -1
  113. package/dist/routes/github-callback.js +43 -21
  114. package/dist/routes/github-callback.js.map +1 -1
  115. package/dist/routes/integrations-settings.d.ts +6 -0
  116. package/dist/routes/integrations-settings.d.ts.map +1 -0
  117. package/dist/routes/integrations-settings.js +7 -0
  118. package/dist/routes/integrations-settings.js.map +1 -0
  119. package/dist/routes/linear-callback.d.ts +2 -0
  120. package/dist/routes/linear-callback.d.ts.map +1 -1
  121. package/dist/routes/linear-callback.js +2 -0
  122. package/dist/routes/linear-callback.js.map +1 -1
  123. package/dist/routes/sentry-callback.d.ts +2 -0
  124. package/dist/routes/sentry-callback.d.ts.map +1 -1
  125. package/dist/routes/sentry-callback.js +2 -0
  126. package/dist/routes/sentry-callback.js.map +1 -1
  127. package/dist/routes/slack-callback.d.ts +2 -0
  128. package/dist/routes/slack-callback.d.ts.map +1 -1
  129. package/dist/routes/slack-callback.js +2 -0
  130. package/dist/routes/slack-callback.js.map +1 -1
  131. package/dist/sentry-callback.d.ts +2 -1
  132. package/dist/sentry-callback.d.ts.map +1 -1
  133. package/dist/sentry-callback.js +16 -19
  134. package/dist/sentry-callback.js.map +1 -1
  135. package/dist/slack-callback.d.ts +2 -1
  136. package/dist/slack-callback.d.ts.map +1 -1
  137. package/dist/slack-callback.js +14 -15
  138. package/dist/slack-callback.js.map +1 -1
  139. package/dist/tsconfig.test.tsbuildinfo +1 -1
  140. package/package.json +17 -17
  141. package/src/application/complete-integration-callback.test.ts +41 -0
  142. package/src/application/complete-integration-callback.ts +39 -0
  143. package/src/components/connection-picker.tsx +7 -7
  144. package/src/components/installed-integrations-section.tsx +29 -24
  145. package/src/components/integration-gallery-for-workspace.tsx +23 -16
  146. package/src/components/integration-gallery.tsx +3 -3
  147. package/src/components/integration-usage-events.ts +1 -116
  148. package/src/components/integration-usage-modal.tsx +4 -4
  149. package/src/components/provider-grid.tsx +30 -18
  150. package/src/components/redirect-install-page.test.tsx +2 -6
  151. package/src/components/redirect-install-page.tsx +3 -2
  152. package/src/components/repository-picker.tsx +6 -6
  153. package/src/components/webhook/webhook-create-modal.tsx +12 -11
  154. package/src/components/webhook/webhook-modals.stories.tsx +10 -10
  155. package/src/components/webhook/webhook-usage-details.tsx +2 -4
  156. package/src/connection-status-badge.tsx +8 -14
  157. package/src/core/models.ts +200 -0
  158. package/src/feature.ts +17 -1
  159. package/src/hooks/api/integration-mapper.ts +64 -0
  160. package/src/hooks/api/integrations.test.ts +44 -22
  161. package/src/hooks/api/integrations.ts +258 -89
  162. package/src/hooks/api/webhook-connections.ts +58 -17
  163. package/src/index.ts +2 -1
  164. package/src/linear-callback.ts +21 -16
  165. package/src/pages/gitea-install-page.tsx +1 -10
  166. package/src/pages/linear-callback-page.test.tsx +1 -1
  167. package/src/pages/linear-callback-page.tsx +18 -29
  168. package/src/pages/linear-install-page.tsx +2 -1
  169. package/src/pages/sentry-callback-page.test.tsx +6 -2
  170. package/src/pages/sentry-callback-page.tsx +28 -25
  171. package/src/pages/sentry-install-page.tsx +2 -1
  172. package/src/pages/slack-callback-page.tsx +20 -25
  173. package/src/pages/slack-install-page.tsx +2 -1
  174. package/src/pages/source-control-onboarding-page.tsx +5 -1
  175. package/src/provider-catalog.ts +7 -6
  176. package/src/routes/github-callback.test.ts +35 -1
  177. package/src/routes/github-callback.tsx +47 -21
  178. package/src/routes/integrations-settings.tsx +4 -0
  179. package/src/routes/linear-callback.tsx +5 -1
  180. package/src/routes/sentry-callback.tsx +5 -1
  181. package/src/routes/slack-callback.tsx +6 -1
  182. package/src/sentry-callback.test.ts +2 -1
  183. package/src/sentry-callback.ts +28 -20
  184. package/src/slack-callback.ts +21 -16
  185. package/test/render.tsx +1 -5
  186. package/tsconfig.build.tsbuildinfo +1 -1
  187. package/vercel.json +5 -0
  188. package/dist/integration-icon.d.ts +0 -17
  189. package/dist/integration-icon.d.ts.map +0 -1
  190. package/dist/integration-icon.js +0 -22
  191. package/dist/integration-icon.js.map +0 -1
  192. package/src/integration-icon.react.test.tsx +0 -40
  193. package/src/integration-icon.test.ts +0 -19
  194. package/src/integration-icon.tsx +0 -26
@@ -1,3 +1,4 @@
1
+ import {PROVIDER_ICONS} from '@shipfox/integration-icons';
1
2
  import type {IconName} from '@shipfox/react-ui/icon';
2
3
 
3
4
  // Literal union (not string) so `<Link to={catalog.setupPath}>` stays typed
@@ -25,31 +26,31 @@ export type ProviderCatalogEntry = RouteProviderCatalogEntry | ModalProviderCata
25
26
  export const PROVIDER_CATALOG: Record<string, ProviderCatalogEntry> = {
26
27
  github: {
27
28
  kind: 'redirect-install',
28
- iconName: 'github',
29
+ iconName: PROVIDER_ICONS.github,
29
30
  setupPath: '/workspaces/$wid/integrations/github',
30
31
  },
31
32
  sentry: {
32
33
  kind: 'redirect-install',
33
- iconName: 'sentry',
34
+ iconName: PROVIDER_ICONS.sentry,
34
35
  setupPath: '/workspaces/$wid/integrations/sentry',
35
36
  },
36
37
  linear: {
37
38
  kind: 'redirect-install',
38
- iconName: 'linear',
39
+ iconName: PROVIDER_ICONS.linear,
39
40
  setupPath: '/workspaces/$wid/integrations/linear',
40
41
  },
41
42
  slack: {
42
43
  kind: 'redirect-install',
43
- iconName: 'slack',
44
+ iconName: PROVIDER_ICONS.slack,
44
45
  setupPath: '/workspaces/$wid/integrations/slack',
45
46
  },
46
47
  gitea: {
47
48
  kind: 'direct-connect',
48
- iconName: 'gitea',
49
+ iconName: PROVIDER_ICONS.gitea,
49
50
  setupPath: '/workspaces/$wid/integrations/gitea',
50
51
  },
51
52
  webhook: {
52
53
  kind: 'modal-connect',
53
- iconName: 'webhookLine',
54
+ iconName: PROVIDER_ICONS.webhook,
54
55
  },
55
56
  };
@@ -1,5 +1,10 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
- import {githubCallbackErrorMessage} from './github-callback.js';
2
+ import {
3
+ githubCallbackErrorMessage,
4
+ githubCallbackParams,
5
+ missingCallbackParams,
6
+ validateGithubCallbackSearch,
7
+ } from './github-callback.js';
3
8
 
4
9
  describe('githubCallbackErrorMessage', () => {
5
10
  it('does not expose an API error message or request URL', () => {
@@ -15,3 +20,32 @@ describe('githubCallbackErrorMessage', () => {
15
20
  expect(message).not.toContain('https://');
16
21
  });
17
22
  });
23
+
24
+ describe('github callback route inputs', () => {
25
+ it('normalizes valid values and reports missing or malformed inputs', () => {
26
+ const search = validateGithubCallbackSearch({
27
+ code: 'grant-code',
28
+ installation_id: '42',
29
+ state: 'signed-state',
30
+ setup_action: 'install',
31
+ });
32
+
33
+ expect(search).toEqual({
34
+ code: 'grant-code',
35
+ installationId: 42,
36
+ state: 'signed-state',
37
+ setupAction: 'install',
38
+ });
39
+ expect(githubCallbackParams(search)).toEqual({
40
+ code: 'grant-code',
41
+ installationId: 42,
42
+ state: 'signed-state',
43
+ setupAction: 'install',
44
+ });
45
+ expect(missingCallbackParams(validateGithubCallbackSearch({code: ['bad'], state: 7}))).toEqual([
46
+ 'code',
47
+ 'installation_id',
48
+ 'state',
49
+ ]);
50
+ });
51
+ });
@@ -1,30 +1,43 @@
1
- import {ApiError, apiRequest} from '@shipfox/client-api';
1
+ import {ApiError} from '@shipfox/client-api';
2
2
  import {useRefreshAuth} from '@shipfox/client-auth';
3
- import {defineRoute} from '@shipfox/client-shell/runtime';
3
+ import {defineRoute, useRouteSearch} from '@shipfox/client-shell/runtime';
4
4
  import {createSingleFlight} from '@shipfox/client-ui';
5
5
  import {FullPageLoader} from '@shipfox/react-ui/loader';
6
6
  import {toast} from '@shipfox/react-ui/toast';
7
- import {useNavigate, useSearch} from '@tanstack/react-router';
7
+ import {useNavigate} from '@tanstack/react-router';
8
8
  import {useEffect} from 'react';
9
+ import {useCompleteIntegrationCallback} from '#application/complete-integration-callback.js';
10
+ import {completeGithubCallback} from '#hooks/api/integrations.js';
9
11
 
10
- interface GithubCallbackParams {
12
+ export interface GithubCallbackParams {
11
13
  code: string;
12
14
  installationId: number;
13
15
  state: string;
14
16
  setupAction?: string;
15
17
  }
16
18
 
19
+ export interface GithubCallbackSearch {
20
+ code?: string;
21
+ installationId?: number;
22
+ state?: string;
23
+ setupAction?: string;
24
+ }
25
+
17
26
  // Retain only recent completions: this bounds long-lived callback pages while
18
27
  // still covering StrictMode and immediate Back/Forward remounts.
19
28
  const callbackRequests = createSingleFlight<string, void>({maxTerminalResults: 32});
20
29
  const toastedCallbacks = new Set<string>();
21
30
 
22
- export default defineRoute({component: GithubCallbackRoute});
31
+ export default defineRoute({
32
+ validateSearch: validateGithubCallbackSearch,
33
+ component: GithubCallbackRoute,
34
+ });
23
35
 
24
36
  function GithubCallbackRoute() {
25
- const search = useSearch({strict: false});
37
+ const search = useRouteSearch(validateGithubCallbackSearch);
26
38
  const navigate = useNavigate();
27
39
  const refreshAuth = useRefreshAuth();
40
+ const completeIntegrationCallback = useCompleteIntegrationCallback();
28
41
 
29
42
  useEffect(() => {
30
43
  const missing = missingCallbackParams(search);
@@ -46,11 +59,11 @@ function GithubCallbackRoute() {
46
59
  const request = callbackRequests.run(
47
60
  key,
48
61
  async () =>
49
- await refreshAuth().then(async (session) => {
50
- await apiRequest(`/integrations/github/callback/api?${key}`, {
51
- headers: {authorization: `Bearer ${session.token}`},
52
- });
53
- }),
62
+ void (await completeIntegrationCallback({
63
+ input: params,
64
+ refreshAuth,
65
+ complete: async (input, token) => await completeGithubCallback({...input, token}),
66
+ })),
54
67
  );
55
68
  request
56
69
  .then(() => {
@@ -72,24 +85,37 @@ function GithubCallbackRoute() {
72
85
  return () => {
73
86
  disposed = true;
74
87
  };
75
- }, [navigate, refreshAuth, search]);
88
+ }, [completeIntegrationCallback, navigate, refreshAuth, search]);
76
89
 
77
90
  return <FullPageLoader />;
78
91
  }
79
92
 
80
- function missingCallbackParams(search: Record<string, unknown>): string[] {
93
+ export function validateGithubCallbackSearch(input: Record<string, unknown>): GithubCallbackSearch {
94
+ const code = stringParam(input.code);
95
+ const installationId = numberParam(input.installation_id);
96
+ const state = stringParam(input.state);
97
+ const setupAction = stringParam(input.setup_action);
98
+ return {
99
+ ...(code ? {code} : {}),
100
+ ...(installationId === undefined ? {} : {installationId}),
101
+ ...(state ? {state} : {}),
102
+ ...(setupAction ? {setupAction} : {}),
103
+ };
104
+ }
105
+
106
+ export function missingCallbackParams(search: GithubCallbackSearch): string[] {
81
107
  const missing: string[] = [];
82
- if (!stringParam(search.code)) missing.push('code');
83
- if (!numberParam(search.installation_id)) missing.push('installation_id');
84
- if (!stringParam(search.state)) missing.push('state');
108
+ if (!search.code) missing.push('code');
109
+ if (search.installationId === undefined) missing.push('installation_id');
110
+ if (!search.state) missing.push('state');
85
111
  return missing;
86
112
  }
87
- function githubCallbackParams(search: Record<string, unknown>): GithubCallbackParams | undefined {
88
- const code = stringParam(search.code);
89
- const installationId = numberParam(search.installation_id);
90
- const state = stringParam(search.state);
113
+ export function githubCallbackParams(
114
+ search: GithubCallbackSearch,
115
+ ): GithubCallbackParams | undefined {
116
+ const {code, installationId, state} = search;
91
117
  if (!code || installationId === undefined || !state) return undefined;
92
- const setupAction = stringParam(search.setup_action);
118
+ const {setupAction} = search;
93
119
  return setupAction ? {code, installationId, state, setupAction} : {code, installationId, state};
94
120
  }
95
121
  function encodeCallbackQuery(params: GithubCallbackParams): string {
@@ -0,0 +1,4 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {IntegrationGallery} from '#index.js';
3
+
4
+ export default defineRoute({component: IntegrationGallery});
@@ -1,4 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {LinearCallbackPage} from '#pages/linear-callback-page.js';
3
+ import {parseLinearCallbackQuery} from '../linear-callback.js';
3
4
 
4
- export default defineRoute({component: LinearCallbackPage});
5
+ export default defineRoute({
6
+ validateSearch: parseLinearCallbackQuery,
7
+ component: LinearCallbackPage,
8
+ });
@@ -1,4 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {SentryCallbackPage} from '#pages/sentry-callback-page.js';
3
+ import {parseSentryCallbackParams} from '../sentry-callback.js';
3
4
 
4
- export default defineRoute({component: SentryCallbackPage});
5
+ export default defineRoute({
6
+ validateSearch: parseSentryCallbackParams,
7
+ component: SentryCallbackPage,
8
+ });
@@ -1,3 +1,8 @@
1
1
  import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {SlackCallbackPage} from '#pages/slack-callback-page.js';
3
- export default defineRoute({component: SlackCallbackPage});
3
+ import {parseSlackCallbackQuery} from '../slack-callback.js';
4
+
5
+ export default defineRoute({
6
+ validateSearch: parseSlackCallbackQuery,
7
+ component: SlackCallbackPage,
8
+ });
@@ -181,7 +181,8 @@ describe('classifySentryConnectError', () => {
181
181
 
182
182
  expect(result).toEqual({
183
183
  kind: 'terminal',
184
- message: 'Sentry rejected the code.',
184
+ message:
185
+ 'Sentry did not accept this install. Start the install again from workspace settings.',
185
186
  startOver: true,
186
187
  });
187
188
  });
@@ -1,34 +1,39 @@
1
1
  import {ApiError} from '@shipfox/client-api';
2
+ import {
3
+ type BrowserStorage,
4
+ type BrowserStorageKey,
5
+ createTypedBrowserStorage,
6
+ } from '@shipfox/client-ui';
2
7
 
3
8
  export const SENTRY_INSTALL_WORKSPACE_KEY = 'shipfox.sentry-install.workspace-id';
4
9
 
5
- type WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
10
+ type WorkspaceStorage = BrowserStorage | undefined;
11
+
12
+ const sentryInstallWorkspaceStorageKey = {
13
+ key: SENTRY_INSTALL_WORKSPACE_KEY,
14
+ lifetime: 'session',
15
+ principalScope: 'workspace',
16
+ serialize: (workspaceId: string) => workspaceId,
17
+ parse: (value: string) => value || undefined,
18
+ } satisfies BrowserStorageKey<string>;
6
19
 
7
20
  // Storage helpers swallow failures (quota, private mode, disabled storage):
8
21
  // the handoff is an optimization, never a requirement — the callback always
9
22
  // asks for explicit confirmation and only uses the stored id to pre-select.
10
23
  export function saveSentryInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {
11
- try {
12
- storage.setItem(SENTRY_INSTALL_WORKSPACE_KEY, workspaceId);
13
- } catch {
14
- // The callback falls back to its sole-workspace pre-selection.
15
- }
24
+ installWorkspaceStorage(storage).write(workspaceId);
16
25
  }
17
26
 
18
27
  export function readSentryInstallWorkspace(storage: WorkspaceStorage): string | undefined {
19
- try {
20
- return storage.getItem(SENTRY_INSTALL_WORKSPACE_KEY) ?? undefined;
21
- } catch {
22
- return undefined;
23
- }
28
+ return installWorkspaceStorage(storage).read();
24
29
  }
25
30
 
26
31
  export function clearSentryInstallWorkspace(storage: WorkspaceStorage): void {
27
- try {
28
- storage.removeItem(SENTRY_INSTALL_WORKSPACE_KEY);
29
- } catch {
30
- // A tab-scoped leftover key is harmless: it only pre-selects.
31
- }
32
+ installWorkspaceStorage(storage).remove();
33
+ }
34
+
35
+ function installWorkspaceStorage(storage: WorkspaceStorage) {
36
+ return createTypedBrowserStorage(() => storage, sentryInstallWorkspaceStorageKey);
32
37
  }
33
38
 
34
39
  export interface SentryCallbackParams {
@@ -108,7 +113,7 @@ export function classifySentryConnectError(error: unknown): SentryConnectFailure
108
113
  return {kind: 'retryable', message: 'Sentry is unreachable. Try again.'};
109
114
  }
110
115
  if (error.status === 0 || error.code === 'network-error') {
111
- // A network failure (apiRequest throws status 0) never reached the
116
+ // A network failure (the client transport reports status 0) never reached the
112
117
  // server, so the grant code is untouched and a plain Retry can recover.
113
118
  // Must precede the status < 500 branch, which would otherwise treat it as
114
119
  // a spent code and force a full restart.
@@ -118,9 +123,12 @@ export function classifySentryConnectError(error: unknown): SentryConnectFailure
118
123
  };
119
124
  }
120
125
  if (error.status < 500) {
121
- // 4xx (access-denied, malformed-provider-response, validation): the grant
122
- // code is spent or rejected — only a fresh install can recover.
123
- return {kind: 'terminal', message: error.message, startOver: true};
126
+ return {
127
+ kind: 'terminal',
128
+ message:
129
+ 'Sentry did not accept this install. Start the install again from workspace settings.',
130
+ startOver: true,
131
+ };
124
132
  }
125
133
  }
126
134
  return {kind: 'retryable', message: 'Could not install Sentry. Try again.'};
@@ -1,29 +1,34 @@
1
1
  import type {SlackCallbackQueryDto} from '@shipfox/api-integration-slack-dto';
2
2
  import {ApiError} from '@shipfox/client-api';
3
+ import {
4
+ type BrowserStorage,
5
+ type BrowserStorageKey,
6
+ createTypedBrowserStorage,
7
+ } from '@shipfox/client-ui';
3
8
 
4
9
  export const SLACK_INSTALL_WORKSPACE_KEY = 'shipfox.slack-install.workspace-id';
5
- type WorkspaceStorage = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
10
+ type WorkspaceStorage = BrowserStorage | undefined;
11
+
12
+ const slackInstallWorkspaceStorageKey = {
13
+ key: SLACK_INSTALL_WORKSPACE_KEY,
14
+ lifetime: 'session',
15
+ principalScope: 'workspace',
16
+ serialize: (workspaceId: string) => workspaceId,
17
+ parse: (value: string) => value || undefined,
18
+ } satisfies BrowserStorageKey<string>;
6
19
 
7
20
  export function saveSlackInstallWorkspace(storage: WorkspaceStorage, workspaceId: string): void {
8
- try {
9
- storage.setItem(SLACK_INSTALL_WORKSPACE_KEY, workspaceId);
10
- } catch {
11
- // Storage only supports recovery navigation.
12
- }
21
+ installWorkspaceStorage(storage).write(workspaceId);
13
22
  }
14
23
  export function readSlackInstallWorkspace(storage: WorkspaceStorage): string | undefined {
15
- try {
16
- return storage.getItem(SLACK_INSTALL_WORKSPACE_KEY) ?? undefined;
17
- } catch {
18
- return undefined;
19
- }
24
+ return installWorkspaceStorage(storage).read();
20
25
  }
21
26
  export function clearSlackInstallWorkspace(storage: WorkspaceStorage): void {
22
- try {
23
- storage.removeItem(SLACK_INSTALL_WORKSPACE_KEY);
24
- } catch {
25
- // A stale recovery hint cannot affect the API callback.
26
- }
27
+ installWorkspaceStorage(storage).remove();
28
+ }
29
+
30
+ function installWorkspaceStorage(storage: WorkspaceStorage) {
31
+ return createTypedBrowserStorage(() => storage, slackInstallWorkspaceStorageKey);
27
32
  }
28
33
 
29
34
  export function parseSlackCallbackQuery(
package/test/render.tsx CHANGED
@@ -41,11 +41,7 @@ function authState(workspaces: TestWorkspace[]): AuthState {
41
41
  user: {
42
42
  id: '22222222-2222-4222-8222-222222222222',
43
43
  email: 'user@example.com',
44
- name: null,
45
- email_verified_at: new Date().toISOString(),
46
- status: 'active',
47
- created_at: new Date().toISOString(),
48
- updated_at: new Date().toISOString(),
44
+ emailVerifiedAt: new Date().toISOString(),
49
45
  },
50
46
  workspaces,
51
47
  };