@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,225 @@
1
+ import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
2
+ import type {WebhookConnectionDto} from '@shipfox/api-integration-webhook-dto';
3
+ import {WEBHOOK_RECEIVED_EVENT} from '@shipfox/api-integration-webhook-dto';
4
+ import {configureApiClient} from '@shipfox/client-api';
5
+ import {Toaster} from '@shipfox/react-ui/toast';
6
+ import type {Meta, StoryObj} from '@storybook/react';
7
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
8
+ import {
9
+ createMemoryHistory,
10
+ createRootRoute,
11
+ createRoute,
12
+ createRouter,
13
+ Outlet,
14
+ RouterProvider,
15
+ } from '@tanstack/react-router';
16
+ import {useMemo} from 'react';
17
+ import {IntegrationDeleteConfirmModal} from '../integration-delete-confirm-modal.js';
18
+ import {IntegrationUsageModal} from '../integration-usage-modal.js';
19
+ import {CopyableValue} from './copyable-value.js';
20
+ import {WebhookCreateModal} from './webhook-create-modal.js';
21
+ import {WebhookPublicEndpointAlert} from './webhook-public-endpoint-alert.js';
22
+ import {WebhookUsageDetails} from './webhook-usage-details.js';
23
+
24
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
25
+ const CONNECTION_ID = '77777777-7777-4777-8777-777777777777';
26
+
27
+ type Scenario =
28
+ | 'create-form'
29
+ | 'create-success'
30
+ | 'usage-active'
31
+ | 'usage-disabled'
32
+ | 'delete-confirm'
33
+ | 'copyable-value';
34
+
35
+ interface WebhookModalStoryProps {
36
+ scenario: Scenario;
37
+ }
38
+
39
+ const activeConnection: WebhookConnectionDto = {
40
+ id: CONNECTION_ID,
41
+ workspace_id: WORKSPACE_ID,
42
+ name: 'Stripe production',
43
+ slug: 'stripe-prod',
44
+ lifecycle_status: 'active',
45
+ inbound_url: 'https://api.example.test/webhook/77777777-7777-4777-8777-777777777777',
46
+ created_at: '2026-04-12T00:00:00.000Z',
47
+ updated_at: '2026-04-12T00:00:00.000Z',
48
+ };
49
+
50
+ const disabledConnection: WebhookConnectionDto = {
51
+ ...activeConnection,
52
+ lifecycle_status: 'disabled',
53
+ };
54
+
55
+ const activeIntegrationConnection: IntegrationConnectionDto = {
56
+ id: CONNECTION_ID,
57
+ workspace_id: WORKSPACE_ID,
58
+ provider: 'webhook',
59
+ external_account_id: activeConnection.slug,
60
+ slug: activeConnection.slug,
61
+ display_name: activeConnection.name,
62
+ lifecycle_status: activeConnection.lifecycle_status,
63
+ capabilities: [],
64
+ created_at: activeConnection.created_at,
65
+ updated_at: activeConnection.updated_at,
66
+ };
67
+
68
+ const disabledIntegrationConnection: IntegrationConnectionDto = {
69
+ ...activeIntegrationConnection,
70
+ lifecycle_status: 'disabled',
71
+ };
72
+
73
+ function WebhookModalStory({scenario}: WebhookModalStoryProps) {
74
+ const connection = scenario === 'usage-disabled' ? disabledConnection : activeConnection;
75
+
76
+ configureApiClient({
77
+ baseUrl: 'https://api.example.test',
78
+ fetchImpl: fetchForConnection(connection),
79
+ });
80
+
81
+ const queryClient = useMemo(
82
+ () => new QueryClient({defaultOptions: {queries: {retry: false}}}),
83
+ [],
84
+ );
85
+ const router = useMemo(() => createStoryRouter(scenario), [scenario]);
86
+
87
+ return (
88
+ <QueryClientProvider client={queryClient}>
89
+ <RouterProvider router={router} />
90
+ <Toaster />
91
+ </QueryClientProvider>
92
+ );
93
+ }
94
+
95
+ const meta = {
96
+ title: 'Integrations/WebhookModals',
97
+ component: WebhookModalStory,
98
+ parameters: {layout: 'fullscreen'},
99
+ args: {scenario: 'create-form'},
100
+ } satisfies Meta<typeof WebhookModalStory>;
101
+
102
+ export default meta;
103
+ type Story = StoryObj<typeof meta>;
104
+
105
+ export const Playground: Story = {};
106
+
107
+ export const CreateSuccess: Story = {
108
+ args: {scenario: 'create-success'},
109
+ };
110
+
111
+ export const UsageActive: Story = {
112
+ args: {scenario: 'usage-active'},
113
+ };
114
+
115
+ export const UsageDisabled: Story = {
116
+ args: {scenario: 'usage-disabled'},
117
+ };
118
+
119
+ export const DeleteConfirm: Story = {
120
+ args: {scenario: 'delete-confirm'},
121
+ };
122
+
123
+ export const CopyableValueState: Story = {
124
+ args: {scenario: 'copyable-value'},
125
+ };
126
+
127
+ function StorySurface({scenario}: {scenario: Scenario}) {
128
+ return (
129
+ <div className="min-h-screen bg-background-neutral-background p-24">
130
+ {scenario === 'create-form' ? (
131
+ <WebhookCreateModal
132
+ workspaceId={WORKSPACE_ID}
133
+ open
134
+ onOpenChange={() => undefined}
135
+ onCreated={() => undefined}
136
+ />
137
+ ) : null}
138
+ {scenario === 'create-success' ||
139
+ scenario === 'usage-active' ||
140
+ scenario === 'usage-disabled' ? (
141
+ <IntegrationUsageModal
142
+ connection={
143
+ scenario === 'usage-disabled'
144
+ ? disabledIntegrationConnection
145
+ : activeIntegrationConnection
146
+ }
147
+ events={[{value: WEBHOOK_RECEIVED_EVENT, label: WEBHOOK_RECEIVED_EVENT}]}
148
+ open
149
+ onOpenChange={() => undefined}
150
+ >
151
+ <WebhookUsageDetails workspaceId={WORKSPACE_ID} connectionId={CONNECTION_ID} />
152
+ </IntegrationUsageModal>
153
+ ) : null}
154
+ {scenario === 'delete-confirm' ? (
155
+ <IntegrationDeleteConfirmModal
156
+ connectionName={activeConnection.name}
157
+ open
158
+ isPending={false}
159
+ onOpenChange={() => undefined}
160
+ onConfirm={() => undefined}
161
+ />
162
+ ) : null}
163
+ {scenario === 'copyable-value' ? (
164
+ <div className="mx-auto max-w-[560px] rounded-8 border border-border-neutral-base bg-background-neutral-base p-24">
165
+ <div className="flex flex-col gap-12">
166
+ <CopyableValue label="inbound URL" value={activeConnection.inbound_url} />
167
+ <WebhookPublicEndpointAlert />
168
+ </div>
169
+ </div>
170
+ ) : null}
171
+ </div>
172
+ );
173
+ }
174
+
175
+ function createStoryRouter(scenario: Scenario) {
176
+ const rootRoute = createRootRoute({component: Outlet});
177
+ const workspaceRoute = createRoute({
178
+ getParentRoute: () => rootRoute,
179
+ path: '/workspaces/$wid',
180
+ component: Outlet,
181
+ });
182
+ const integrationsRoute = createRoute({
183
+ getParentRoute: () => workspaceRoute,
184
+ path: 'settings/integrations',
185
+ component: () => <StorySurface scenario={scenario} />,
186
+ });
187
+ const eventsRoute = createRoute({
188
+ getParentRoute: () => workspaceRoute,
189
+ path: 'settings/events',
190
+ component: () => <div />,
191
+ });
192
+
193
+ return createRouter({
194
+ history: createMemoryHistory({
195
+ initialEntries: [`/workspaces/${WORKSPACE_ID}/settings/integrations`],
196
+ }),
197
+ routeTree: rootRoute.addChildren([
198
+ workspaceRoute.addChildren([integrationsRoute, eventsRoute]),
199
+ ]),
200
+ });
201
+ }
202
+
203
+ function fetchForConnection(connection: WebhookConnectionDto): typeof fetch {
204
+ return (input) => {
205
+ const url = requestUrl(input);
206
+ if (url.pathname === '/integrations/webhook/connections') {
207
+ return Promise.resolve(jsonResponse({connections: [connection]}));
208
+ }
209
+ return Promise.resolve(jsonResponse({}, {status: 404}));
210
+ };
211
+ }
212
+
213
+ function requestUrl(input: RequestInfo | URL): URL {
214
+ if (typeof input === 'string') return new URL(input);
215
+ if (input instanceof URL) return input;
216
+ return new URL(input.url);
217
+ }
218
+
219
+ function jsonResponse(body: unknown, init: ResponseInit = {}) {
220
+ return new Response(JSON.stringify(body), {
221
+ status: 200,
222
+ headers: {'content-type': 'application/json'},
223
+ ...init,
224
+ });
225
+ }
@@ -0,0 +1,15 @@
1
+ import {Callout, CalloutContent, CalloutDescription, CalloutTitle} from '@shipfox/react-ui/callout';
2
+
3
+ export function WebhookPublicEndpointAlert() {
4
+ return (
5
+ <Callout role="alert" type="default">
6
+ <CalloutContent>
7
+ <CalloutTitle>Public webhook endpoint</CalloutTitle>
8
+ <CalloutDescription>
9
+ Anyone with this URL can send webhook events to this source. You are responsible for
10
+ verifying the received webhook in your workflow before trusting its payload.
11
+ </CalloutDescription>
12
+ </CalloutContent>
13
+ </Callout>
14
+ );
15
+ }
@@ -0,0 +1,44 @@
1
+ import {QueryLoadError} from '@shipfox/client-ui';
2
+ import {ShipfoxLoader} from '@shipfox/react-ui/loader';
3
+ import {Text} from '@shipfox/react-ui/typography';
4
+ import {useWebhookConnectionsQuery} from '#hooks/api/webhook-connections.js';
5
+ import {CopyableValue} from './copyable-value.js';
6
+ import {WebhookPublicEndpointAlert} from './webhook-public-endpoint-alert.js';
7
+
8
+ interface WebhookUsageDetailsProps {
9
+ workspaceId: string;
10
+ connectionId: string;
11
+ }
12
+
13
+ export function WebhookUsageDetails({workspaceId, connectionId}: WebhookUsageDetailsProps) {
14
+ const connectionsQuery = useWebhookConnectionsQuery(workspaceId);
15
+ const connection = connectionsQuery.data?.connections.find(
16
+ (candidate) => candidate.id === connectionId,
17
+ );
18
+
19
+ if (connectionsQuery.isPending) {
20
+ return (
21
+ <div className="flex min-h-80 items-center justify-center">
22
+ <ShipfoxLoader size={32} animation="circular" color="orange" background="light" />
23
+ </div>
24
+ );
25
+ }
26
+
27
+ if (connectionsQuery.isError && connectionsQuery.data === undefined) {
28
+ return <QueryLoadError query={connectionsQuery} subject="webhook details" />;
29
+ }
30
+
31
+ if (!connection) return null;
32
+
33
+ return (
34
+ <div className="flex w-full flex-col gap-16">
35
+ <div className="flex w-full min-w-0 flex-col gap-8">
36
+ <Text size="sm" bold>
37
+ Inbound URL
38
+ </Text>
39
+ <CopyableValue label="inbound URL" value={connection.inbound_url} />
40
+ <WebhookPublicEndpointAlert />
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,38 @@
1
+ import type {IntegrationConnectionLifecycleStatusDto} from '@shipfox/api-integration-core-dto';
2
+ import {Badge} from '@shipfox/react-ui/badge';
3
+ import type {IconName} from '@shipfox/react-ui/icon';
4
+
5
+ const lifecyclePills: Record<
6
+ IntegrationConnectionLifecycleStatusDto,
7
+ {variant: 'warning' | 'error'; label: string; iconLeft?: IconName} | undefined
8
+ > = {
9
+ // `active` is the expected state and carries no badge.
10
+ active: undefined,
11
+ disabled: {variant: 'warning', label: 'Disabled', iconLeft: 'errorWarningLine'},
12
+ error: {variant: 'error', label: 'Error'},
13
+ };
14
+
15
+ export interface ConnectionStatusBadgeProps {
16
+ status: IntegrationConnectionLifecycleStatusDto;
17
+ className?: string;
18
+ }
19
+
20
+ /**
21
+ * Single source of truth for a connection's lifecycle pill, shared by the
22
+ * integration gallery and any surface that reflects source health. Renders
23
+ * nothing for the expected `active` state.
24
+ */
25
+ export function ConnectionStatusBadge({status, className}: ConnectionStatusBadgeProps) {
26
+ const pill = lifecyclePills[status];
27
+ if (!pill) return null;
28
+ return (
29
+ <Badge
30
+ variant={pill.variant}
31
+ radius="rounded"
32
+ className={className}
33
+ {...(pill.iconLeft ? {iconLeft: pill.iconLeft} : {})}
34
+ >
35
+ {pill.label}
36
+ </Badge>
37
+ );
38
+ }
package/src/feature.ts ADDED
@@ -0,0 +1,47 @@
1
+ import {defineClientFeature} from '@shipfox/client-shell';
2
+
3
+ export const integrationsFeature = defineClientFeature({
4
+ id: 'shipfox.integrations',
5
+ routes: [
6
+ {
7
+ path: '/integrations/github/callback',
8
+ parent: 'root',
9
+ impl: '@shipfox/client-integrations/routes/github-callback',
10
+ },
11
+ {
12
+ path: '/integrations/linear/callback',
13
+ parent: 'root',
14
+ impl: '@shipfox/client-integrations/routes/linear-callback',
15
+ },
16
+ {
17
+ path: '/integrations/sentry/callback',
18
+ parent: 'root',
19
+ impl: '@shipfox/client-integrations/routes/sentry-callback',
20
+ },
21
+ {
22
+ path: '/workspaces/$wid/integrations',
23
+ parent: 'workspaceLayout',
24
+ impl: '@shipfox/client-integrations/routes/integrations',
25
+ },
26
+ {
27
+ path: '/workspaces/$wid/integrations/gitea',
28
+ parent: 'workspaceLayout',
29
+ impl: '@shipfox/client-integrations/routes/gitea',
30
+ },
31
+ {
32
+ path: '/workspaces/$wid/integrations/github',
33
+ parent: 'workspaceLayout',
34
+ impl: '@shipfox/client-integrations/routes/github',
35
+ },
36
+ {
37
+ path: '/workspaces/$wid/integrations/linear',
38
+ parent: 'workspaceLayout',
39
+ impl: '@shipfox/client-integrations/routes/linear',
40
+ },
41
+ {
42
+ path: '/workspaces/$wid/integrations/sentry',
43
+ parent: 'workspaceLayout',
44
+ impl: '@shipfox/client-integrations/routes/sentry',
45
+ },
46
+ ],
47
+ });
@@ -0,0 +1,105 @@
1
+ import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';
2
+ import {configureApiClient} from '@shipfox/client-api';
3
+ import {
4
+ completeLinearCallback,
5
+ createLinearInstall,
6
+ listSourceConnections,
7
+ } from './integrations.js';
8
+
9
+ function connection(overrides: Partial<IntegrationConnectionDto> = {}): IntegrationConnectionDto {
10
+ return {
11
+ id: 'c1',
12
+ workspace_id: 'ws-1',
13
+ provider: 'github',
14
+ external_account_id: 'acct',
15
+ slug: 'github_acct',
16
+ display_name: 'GitHub',
17
+ lifecycle_status: 'active',
18
+ capabilities: ['source_control'],
19
+ created_at: '2026-01-01T00:00:00.000Z',
20
+ updated_at: '2026-01-01T00:00:00.000Z',
21
+ ...overrides,
22
+ };
23
+ }
24
+
25
+ function jsonResponse(body: unknown) {
26
+ return new Response(JSON.stringify(body), {
27
+ status: 200,
28
+ headers: {'content-type': 'application/json'},
29
+ });
30
+ }
31
+
32
+ describe('listSourceConnections', () => {
33
+ test('requests the source_control capability and drops non-active connections', async () => {
34
+ let requestedUrl = '';
35
+ const fetchImpl = vi.fn((input: RequestInfo | URL) => {
36
+ requestedUrl =
37
+ typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
38
+ return Promise.resolve(
39
+ jsonResponse({
40
+ connections: [
41
+ connection({id: 'active-1', lifecycle_status: 'active'}),
42
+ connection({id: 'disabled-1', lifecycle_status: 'disabled'}),
43
+ connection({id: 'error-1', lifecycle_status: 'error'}),
44
+ ],
45
+ }),
46
+ );
47
+ });
48
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
49
+
50
+ const result = await listSourceConnections({workspaceId: 'ws-1'});
51
+
52
+ expect(requestedUrl).toContain('capability=source_control');
53
+ expect(result.connections.map((connection) => connection.id)).toEqual(['active-1']);
54
+ });
55
+ });
56
+
57
+ describe('Linear transport', () => {
58
+ it('posts the install workspace and forwards an authenticated callback query', async () => {
59
+ const requests: Request[] = [];
60
+ const fetchImpl = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
61
+ requests.push(new Request(input, init));
62
+ return Promise.resolve(
63
+ jsonResponse({
64
+ id: 'connection-1',
65
+ workspace_id: '11111111-1111-4111-8111-111111111111',
66
+ provider: 'linear',
67
+ external_account_id: 'linear-org',
68
+ slug: 'linear_org',
69
+ display_name: 'Linear org',
70
+ lifecycle_status: 'active',
71
+ capabilities: [],
72
+ created_at: '2026-01-01T00:00:00.000Z',
73
+ updated_at: '2026-01-01T00:00:00.000Z',
74
+ }),
75
+ );
76
+ });
77
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
78
+
79
+ await createLinearInstall({workspace_id: '11111111-1111-4111-8111-111111111111'});
80
+ await completeLinearCallback({
81
+ query: {code: 'grant code', state: 'signed state'},
82
+ token: 'session-token',
83
+ });
84
+ await completeLinearCallback({
85
+ query: {
86
+ error: 'access_denied',
87
+ error_description: 'User denied access',
88
+ state: 'signed error state',
89
+ },
90
+ token: 'session-token',
91
+ });
92
+
93
+ expect(requests[0]?.url).toBe('https://api.example.test/integrations/linear/install');
94
+ expect(await requests[0]?.json()).toEqual({
95
+ workspace_id: '11111111-1111-4111-8111-111111111111',
96
+ });
97
+ expect(requests[1]?.url).toBe(
98
+ 'https://api.example.test/integrations/linear/callback/api?code=grant+code&state=signed+state',
99
+ );
100
+ expect(requests[1]?.headers.get('authorization')).toBe('Bearer session-token');
101
+ expect(requests[2]?.url).toBe(
102
+ 'https://api.example.test/integrations/linear/callback/api?error=access_denied&error_description=User+denied+access&state=signed+error+state',
103
+ );
104
+ });
105
+ });