@ekanos/sdk 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -33
- package/api-report.md +15 -4
- package/dist/components/base-activation-dialog.d.ts +15 -5
- package/dist/components/base-activation-dialog.js +31 -5
- package/dist/components/base-activation-dialog.js.map +1 -1
- package/dist/components/base-activation-form.d.ts +20 -5
- package/dist/components/base-activation-form.js +12 -5
- package/dist/components/base-activation-form.js.map +1 -1
- package/dist/components/base-marketplace-tile.js +12 -10
- package/dist/components/base-marketplace-tile.js.map +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/components/oauth-activation-form.d.ts +23 -16
- package/dist/components/oauth-activation-form.js +34 -20
- package/dist/components/oauth-activation-form.js.map +1 -1
- package/dist/components/widgets/use-relative-time.d.ts +7 -0
- package/dist/components/widgets/use-relative-time.js +55 -0
- package/dist/components/widgets/use-relative-time.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -1
- package/dist/components/widgets/widget-context.d.ts +21 -0
- package/dist/components/widgets/widget-context.js.map +1 -1
- package/dist/components/widgets/widget-preview-provider.d.ts +1 -1
- package/dist/components/widgets/widget-preview-provider.js +1 -1
- package/dist/components/widgets/widget-preview-provider.js.map +1 -1
- package/dist/components/widgets/widget-state-components.d.ts +4 -1
- package/dist/components/widgets/widget-state-components.js +6 -4
- package/dist/components/widgets/widget-state-components.js.map +1 -1
- package/dist/components/widgets/widget.d.ts +47 -2
- package/dist/components/widgets/widget.js +111 -23
- package/dist/components/widgets/widget.js.map +1 -1
- package/dist/context/egress.js.map +1 -1
- package/dist/context/storage-validation.d.ts +24 -4
- package/dist/context/storage-validation.js +5 -2
- package/dist/context/storage-validation.js.map +1 -1
- package/dist/errors/public-action-error.d.ts +31 -0
- package/dist/errors/public-action-error.js +35 -0
- package/dist/errors/public-action-error.js.map +1 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -1
- package/dist/hooks/fetch-integration-storage.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/partner-oauth.js +3 -0
- package/dist/hooks/partner-oauth.js.map +1 -1
- package/dist/hooks/use-integration-action.d.ts +105 -0
- package/dist/hooks/use-integration-action.js +134 -0
- package/dist/hooks/use-integration-action.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +78 -78
- package/dist/hooks/use-oauth-connection-status.js +17 -10
- package/dist/hooks/use-oauth-connection-status.js.map +1 -1
- package/dist/hooks/widget-event-context.js.map +1 -1
- package/dist/integration/event-surfaces.js.map +1 -1
- package/dist/integration/index.d.ts +3 -2
- package/dist/integration/index.js +5 -1
- package/dist/integration/index.js.map +1 -1
- package/dist/testing/invoke.js +9 -5
- package/dist/testing/invoke.js.map +1 -1
- package/dist/testing/mock-context.js +8 -7
- package/dist/testing/mock-context.js.map +1 -1
- package/dist/testing/mock-fetch-provider.js.map +1 -1
- package/dist/types/integration.d.ts +42 -24
- package/dist/types/integration.js.map +1 -1
- package/dist/types/widget-ask-context.d.ts +9 -0
- package/dist/types/widget-ask-context.js.map +1 -1
- package/dist/types/workspace-target.d.ts +3 -2
- package/dist/types/workspace-target.js.map +1 -1
- package/eslint.preset.mjs +9 -8
- package/package.json +37 -41
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the widget-actions transport.
|
|
3
|
+
*
|
|
4
|
+
* A partner's `actions` declaration on `IntegrationDefinition` is a
|
|
5
|
+
* record of named, deterministic server handlers —
|
|
6
|
+
* `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
|
|
7
|
+
* button), never synthesized by the AI assistant. The host serves one
|
|
8
|
+
* generic route per action:
|
|
9
|
+
*
|
|
10
|
+
* POST /api/integrations/[slug]/actions/[actionName]
|
|
11
|
+
*
|
|
12
|
+
* and this hook is the only place a partner package should build that
|
|
13
|
+
* request, so a route move is one SDK change rather than a hunt through
|
|
14
|
+
* every widget — the same discipline `partnerOAuthConnectPath`/
|
|
15
|
+
* `fetchIntegrationStorage` already apply to their own routes.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
|
|
19
|
+
* 'acme-crm',
|
|
20
|
+
* 'create-record',
|
|
21
|
+
* { accountId },
|
|
22
|
+
* );
|
|
23
|
+
*
|
|
24
|
+
* <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* ── Typing is a convention, not a cross-package mechanism ───────────────────
|
|
28
|
+
*
|
|
29
|
+
* `@ekanos/sdk` is a separately published package — it cannot see a specific
|
|
30
|
+
* partner's `z.ZodTypeAny` instance, since that object lives in the
|
|
31
|
+
* partner's own server bundle and never ships to the browser, let alone into
|
|
32
|
+
* the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
|
|
33
|
+
* the CALLER supplies by hand at the call site — typically by also exporting
|
|
34
|
+
* `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
|
|
35
|
+
* package that declares the action and importing it into the widget. This
|
|
36
|
+
* hook treats the wire payload as `unknown` at runtime — it never parses it
|
|
37
|
+
* against the partner's own zod schema, only against the success/error
|
|
38
|
+
* envelope shape — so drift between the declared action and the widget's
|
|
39
|
+
* type import is a partner-package refactor problem, not a network-boundary
|
|
40
|
+
* one. Same discipline `fetchIntegrationStorage` already documents for
|
|
41
|
+
* storage ("Values are returned raw and unvalidated").
|
|
42
|
+
*
|
|
43
|
+
* ── Errors are typed, never raw fetch/transport exceptions ──────────────────
|
|
44
|
+
*
|
|
45
|
+
* A rejected mutation's `error` is always an `IntegrationActionError` —
|
|
46
|
+
* never a bare `Error`, a `Response`, or an unwrapped parse/network
|
|
47
|
+
* exception. `status` and `code` are always present; `issues` (the partner's
|
|
48
|
+
* own zod issues) is present only for the route's 400 `invalid_input`
|
|
49
|
+
* response, so a form can map failures back onto fields without
|
|
50
|
+
* string-matching a message.
|
|
51
|
+
*/
|
|
52
|
+
import { type UseMutationResult } from '@tanstack/react-query';
|
|
53
|
+
export interface UseIntegrationActionOptions {
|
|
54
|
+
/** The account the action runs against. Must be one the caller belongs to. */
|
|
55
|
+
accountId: string;
|
|
56
|
+
/**
|
|
57
|
+
* Overrides the base path. Exists for harnesses and tests that serve the
|
|
58
|
+
* route from somewhere other than the host's own origin; production code
|
|
59
|
+
* should leave it alone.
|
|
60
|
+
*/
|
|
61
|
+
baseUrl?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Builds the host actions-route URL for one integration/action pair.
|
|
65
|
+
* Exported because a harness or a test double needs to recognize the exact
|
|
66
|
+
* URL this hook will request.
|
|
67
|
+
*/
|
|
68
|
+
export declare function integrationActionUrl(integrationSlug: string, actionName: string, options?: {
|
|
69
|
+
baseUrl?: string;
|
|
70
|
+
}): string;
|
|
71
|
+
/**
|
|
72
|
+
* Route-defined failure codes the host actions route answers with, plus two
|
|
73
|
+
* transport-level codes this hook assigns itself when the response never
|
|
74
|
+
* reaches a valid envelope: `'network_error'` (the request itself failed —
|
|
75
|
+
* offline, DNS, aborted) and `'invalid_response'` (a response came back but
|
|
76
|
+
* its body isn't the envelope shape this transport expects).
|
|
77
|
+
*/
|
|
78
|
+
export type IntegrationActionErrorCode = 'action_not_found' | 'rate_limited' | 'invalid_input' | 'handler_error' | 'network_error' | 'invalid_response' | (string & {});
|
|
79
|
+
export interface IntegrationActionErrorDetails {
|
|
80
|
+
/** HTTP status; `0` means the request never reached the server. */
|
|
81
|
+
status: number;
|
|
82
|
+
code: IntegrationActionErrorCode;
|
|
83
|
+
/** The partner's own zod issues — present only for a 400 `invalid_input` response. */
|
|
84
|
+
issues?: unknown[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The typed failure a rejected `useIntegrationAction` mutation always throws.
|
|
88
|
+
* `status`/`code` let a caller branch without string-matching `message`;
|
|
89
|
+
* `issues` lets a form map the partner's own validation failures back onto
|
|
90
|
+
* fields.
|
|
91
|
+
*/
|
|
92
|
+
export declare class IntegrationActionError extends Error {
|
|
93
|
+
readonly status: number;
|
|
94
|
+
readonly code: IntegrationActionErrorCode;
|
|
95
|
+
readonly issues?: unknown[];
|
|
96
|
+
constructor(message: string, details: IntegrationActionErrorDetails);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
|
|
100
|
+
* take the action's `TInput` directly — the caller's own already-validated
|
|
101
|
+
* values, no assistant and no natural-language round trip in between. A
|
|
102
|
+
* resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
|
|
103
|
+
* an `IntegrationActionError` (see the module doc comment above).
|
|
104
|
+
*/
|
|
105
|
+
export declare function useIntegrationAction<TInput = unknown, TOutput = unknown>(integrationSlug: string, actionName: string, options: UseIntegrationActionOptions): UseMutationResult<TOutput, IntegrationActionError, TInput>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
/**
|
|
3
|
+
* The browser half of the widget-actions transport.
|
|
4
|
+
*
|
|
5
|
+
* A partner's `actions` declaration on `IntegrationDefinition` is a
|
|
6
|
+
* record of named, deterministic server handlers —
|
|
7
|
+
* `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save
|
|
8
|
+
* button), never synthesized by the AI assistant. The host serves one
|
|
9
|
+
* generic route per action:
|
|
10
|
+
*
|
|
11
|
+
* POST /api/integrations/[slug]/actions/[actionName]
|
|
12
|
+
*
|
|
13
|
+
* and this hook is the only place a partner package should build that
|
|
14
|
+
* request, so a route move is one SDK change rather than a hunt through
|
|
15
|
+
* every widget — the same discipline `partnerOAuthConnectPath`/
|
|
16
|
+
* `fetchIntegrationStorage` already apply to their own routes.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(
|
|
20
|
+
* 'acme-crm',
|
|
21
|
+
* 'create-record',
|
|
22
|
+
* { accountId },
|
|
23
|
+
* );
|
|
24
|
+
*
|
|
25
|
+
* <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* ── Typing is a convention, not a cross-package mechanism ───────────────────
|
|
29
|
+
*
|
|
30
|
+
* `@ekanos/sdk` is a separately published package — it cannot see a specific
|
|
31
|
+
* partner's `z.ZodTypeAny` instance, since that object lives in the
|
|
32
|
+
* partner's own server bundle and never ships to the browser, let alone into
|
|
33
|
+
* the SDK's own compiled types. `TInput`/`TOutput` are generic parameters
|
|
34
|
+
* the CALLER supplies by hand at the call site — typically by also exporting
|
|
35
|
+
* `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same
|
|
36
|
+
* package that declares the action and importing it into the widget. This
|
|
37
|
+
* hook treats the wire payload as `unknown` at runtime — it never parses it
|
|
38
|
+
* against the partner's own zod schema, only against the success/error
|
|
39
|
+
* envelope shape — so drift between the declared action and the widget's
|
|
40
|
+
* type import is a partner-package refactor problem, not a network-boundary
|
|
41
|
+
* one. Same discipline `fetchIntegrationStorage` already documents for
|
|
42
|
+
* storage ("Values are returned raw and unvalidated").
|
|
43
|
+
*
|
|
44
|
+
* ── Errors are typed, never raw fetch/transport exceptions ──────────────────
|
|
45
|
+
*
|
|
46
|
+
* A rejected mutation's `error` is always an `IntegrationActionError` —
|
|
47
|
+
* never a bare `Error`, a `Response`, or an unwrapped parse/network
|
|
48
|
+
* exception. `status` and `code` are always present; `issues` (the partner's
|
|
49
|
+
* own zod issues) is present only for the route's 400 `invalid_input`
|
|
50
|
+
* response, so a form can map failures back onto fields without
|
|
51
|
+
* string-matching a message.
|
|
52
|
+
*/
|
|
53
|
+
import { useMutation } from '@tanstack/react-query';
|
|
54
|
+
const DEFAULT_BASE_URL = '/api/integrations';
|
|
55
|
+
/**
|
|
56
|
+
* Builds the host actions-route URL for one integration/action pair.
|
|
57
|
+
* Exported because a harness or a test double needs to recognize the exact
|
|
58
|
+
* URL this hook will request.
|
|
59
|
+
*/
|
|
60
|
+
export function integrationActionUrl(integrationSlug, actionName, options = {}) {
|
|
61
|
+
var _a;
|
|
62
|
+
const base = (_a = options.baseUrl) !== null && _a !== void 0 ? _a : DEFAULT_BASE_URL;
|
|
63
|
+
return `${base}/${integrationSlug}/actions/${actionName}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The typed failure a rejected `useIntegrationAction` mutation always throws.
|
|
67
|
+
* `status`/`code` let a caller branch without string-matching `message`;
|
|
68
|
+
* `issues` lets a form map the partner's own validation failures back onto
|
|
69
|
+
* fields.
|
|
70
|
+
*/
|
|
71
|
+
export class IntegrationActionError extends Error {
|
|
72
|
+
constructor(message, details) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = 'IntegrationActionError';
|
|
75
|
+
this.status = details.status;
|
|
76
|
+
this.code = details.code;
|
|
77
|
+
this.issues = details.issues;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isSuccessEnvelope(value) {
|
|
81
|
+
return (typeof value === 'object' &&
|
|
82
|
+
value !== null &&
|
|
83
|
+
value.success === true &&
|
|
84
|
+
'data' in value);
|
|
85
|
+
}
|
|
86
|
+
function isErrorEnvelope(value) {
|
|
87
|
+
return (typeof value === 'object' &&
|
|
88
|
+
value !== null &&
|
|
89
|
+
value.success === false &&
|
|
90
|
+
typeof value.error === 'string');
|
|
91
|
+
}
|
|
92
|
+
async function runIntegrationAction(integrationSlug, actionName, input, options) {
|
|
93
|
+
var _a;
|
|
94
|
+
const url = integrationActionUrl(integrationSlug, actionName, options);
|
|
95
|
+
let response;
|
|
96
|
+
try {
|
|
97
|
+
response = await globalThis.fetch(url, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'content-type': 'application/json',
|
|
101
|
+
accept: 'application/json',
|
|
102
|
+
},
|
|
103
|
+
body: JSON.stringify({ accountId: options.accountId, input }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
throw new IntegrationActionError(`Could not reach '${integrationSlug}/${actionName}' (${error instanceof Error ? error.message : 'network error'}).`, { status: 0, code: 'network_error' });
|
|
108
|
+
}
|
|
109
|
+
const payload = await response.json().catch(() => null);
|
|
110
|
+
if (response.ok && isSuccessEnvelope(payload)) {
|
|
111
|
+
return payload.data;
|
|
112
|
+
}
|
|
113
|
+
if (isErrorEnvelope(payload)) {
|
|
114
|
+
throw new IntegrationActionError(payload.error, {
|
|
115
|
+
status: response.status,
|
|
116
|
+
code: (_a = payload.code) !== null && _a !== void 0 ? _a : 'handler_error',
|
|
117
|
+
issues: payload.issues,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw new IntegrationActionError(`Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`, { status: response.status, code: 'invalid_response' });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Invokes a declared partner action from a widget. `mutate`/`mutateAsync`
|
|
124
|
+
* take the action's `TInput` directly — the caller's own already-validated
|
|
125
|
+
* values, no assistant and no natural-language round trip in between. A
|
|
126
|
+
* resolved mutation's `data` is `TOutput`; a rejected one's `error` is always
|
|
127
|
+
* an `IntegrationActionError` (see the module doc comment above).
|
|
128
|
+
*/
|
|
129
|
+
export function useIntegrationAction(integrationSlug, actionName, options) {
|
|
130
|
+
return useMutation({
|
|
131
|
+
mutationFn: (input) => runIntegrationAction(integrationSlug, actionName, input, options),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=use-integration-action.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-integration-action.js","sourceRoot":"","sources":["../../src/hooks/use-integration-action.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n/**\n * The browser half of the widget-actions transport.\n *\n * A partner's `actions` declaration on `IntegrationDefinition` is a\n * record of named, deterministic server handlers —\n * `(ctx, input) => Promise<unknown>` — invoked by a widget's own UI (a Save\n * button), never synthesized by the AI assistant. The host serves one\n * generic route per action:\n *\n * POST /api/integrations/[slug]/actions/[actionName]\n *\n * and this hook is the only place a partner package should build that\n * request, so a route move is one SDK change rather than a hunt through\n * every widget — the same discipline `partnerOAuthConnectPath`/\n * `fetchIntegrationStorage` already apply to their own routes.\n *\n * ```tsx\n * const createRecord = useIntegrationAction<CreateRecordInput, CreateRecordOutput>(\n * 'acme-crm',\n * 'create-record',\n * { accountId },\n * );\n *\n * <Button onClick={() => createRecord.mutate(formValues)}>Save</Button>\n * ```\n *\n * ── Typing is a convention, not a cross-package mechanism ───────────────────\n *\n * `@ekanos/sdk` is a separately published package — it cannot see a specific\n * partner's `z.ZodTypeAny` instance, since that object lives in the\n * partner's own server bundle and never ships to the browser, let alone into\n * the SDK's own compiled types. `TInput`/`TOutput` are generic parameters\n * the CALLER supplies by hand at the call site — typically by also exporting\n * `type CreateRecordInput = z.infer<typeof createRecordSchema>` from the same\n * package that declares the action and importing it into the widget. This\n * hook treats the wire payload as `unknown` at runtime — it never parses it\n * against the partner's own zod schema, only against the success/error\n * envelope shape — so drift between the declared action and the widget's\n * type import is a partner-package refactor problem, not a network-boundary\n * one. Same discipline `fetchIntegrationStorage` already documents for\n * storage (\"Values are returned raw and unvalidated\").\n *\n * ── Errors are typed, never raw fetch/transport exceptions ──────────────────\n *\n * A rejected mutation's `error` is always an `IntegrationActionError` —\n * never a bare `Error`, a `Response`, or an unwrapped parse/network\n * exception. `status` and `code` are always present; `issues` (the partner's\n * own zod issues) is present only for the route's 400 `invalid_input`\n * response, so a form can map failures back onto fields without\n * string-matching a message.\n */\nimport { type UseMutationResult, useMutation } from '@tanstack/react-query';\n\nconst DEFAULT_BASE_URL = '/api/integrations';\n\nexport interface UseIntegrationActionOptions {\n /** The account the action runs against. Must be one the caller belongs to. */\n accountId: string;\n /**\n * Overrides the base path. Exists for harnesses and tests that serve the\n * route from somewhere other than the host's own origin; production code\n * should leave it alone.\n */\n baseUrl?: string;\n}\n\n/**\n * Builds the host actions-route URL for one integration/action pair.\n * Exported because a harness or a test double needs to recognize the exact\n * URL this hook will request.\n */\nexport function integrationActionUrl(\n integrationSlug: string,\n actionName: string,\n options: { baseUrl?: string } = {},\n): string {\n const base = options.baseUrl ?? DEFAULT_BASE_URL;\n return `${base}/${integrationSlug}/actions/${actionName}`;\n}\n\n/**\n * Route-defined failure codes the host actions route answers with, plus two\n * transport-level codes this hook assigns itself when the response never\n * reaches a valid envelope: `'network_error'` (the request itself failed —\n * offline, DNS, aborted) and `'invalid_response'` (a response came back but\n * its body isn't the envelope shape this transport expects).\n */\nexport type IntegrationActionErrorCode =\n | 'action_not_found'\n | 'rate_limited'\n | 'invalid_input'\n | 'handler_error'\n | 'network_error'\n | 'invalid_response'\n | (string & {});\n\nexport interface IntegrationActionErrorDetails {\n /** HTTP status; `0` means the request never reached the server. */\n status: number;\n code: IntegrationActionErrorCode;\n /** The partner's own zod issues — present only for a 400 `invalid_input` response. */\n issues?: unknown[];\n}\n\n/**\n * The typed failure a rejected `useIntegrationAction` mutation always throws.\n * `status`/`code` let a caller branch without string-matching `message`;\n * `issues` lets a form map the partner's own validation failures back onto\n * fields.\n */\nexport class IntegrationActionError extends Error {\n readonly status: number;\n readonly code: IntegrationActionErrorCode;\n readonly issues?: unknown[];\n\n constructor(message: string, details: IntegrationActionErrorDetails) {\n super(message);\n this.name = 'IntegrationActionError';\n this.status = details.status;\n this.code = details.code;\n this.issues = details.issues;\n }\n}\n\ninterface ActionSuccessEnvelope {\n success: true;\n data: unknown;\n}\n\ninterface ActionErrorEnvelope {\n success: false;\n error: string;\n code?: string;\n issues?: unknown[];\n}\n\nfunction isSuccessEnvelope(value: unknown): value is ActionSuccessEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === true &&\n 'data' in value\n );\n}\n\nfunction isErrorEnvelope(value: unknown): value is ActionErrorEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { success?: unknown }).success === false &&\n typeof (value as { error?: unknown }).error === 'string'\n );\n}\n\nasync function runIntegrationAction<TInput, TOutput>(\n integrationSlug: string,\n actionName: string,\n input: TInput,\n options: UseIntegrationActionOptions,\n): Promise<TOutput> {\n const url = integrationActionUrl(integrationSlug, actionName, options);\n\n let response: Response;\n try {\n response = await globalThis.fetch(url, {\n method: 'POST',\n headers: {\n 'content-type': 'application/json',\n accept: 'application/json',\n },\n body: JSON.stringify({ accountId: options.accountId, input }),\n });\n } catch (error) {\n throw new IntegrationActionError(\n `Could not reach '${integrationSlug}/${actionName}' (${\n error instanceof Error ? error.message : 'network error'\n }).`,\n { status: 0, code: 'network_error' },\n );\n }\n\n const payload: unknown = await response.json().catch(() => null);\n\n if (response.ok && isSuccessEnvelope(payload)) {\n return payload.data as TOutput;\n }\n\n if (isErrorEnvelope(payload)) {\n throw new IntegrationActionError(payload.error, {\n status: response.status,\n code: payload.code ?? 'handler_error',\n issues: payload.issues,\n });\n }\n\n throw new IntegrationActionError(\n `Could not run '${integrationSlug}/${actionName}' (${response.status} ${response.statusText}).`,\n { status: response.status, code: 'invalid_response' },\n );\n}\n\n/**\n * Invokes a declared partner action from a widget. `mutate`/`mutateAsync`\n * take the action's `TInput` directly — the caller's own already-validated\n * values, no assistant and no natural-language round trip in between. A\n * resolved mutation's `data` is `TOutput`; a rejected one's `error` is always\n * an `IntegrationActionError` (see the module doc comment above).\n */\nexport function useIntegrationAction<TInput = unknown, TOutput = unknown>(\n integrationSlug: string,\n actionName: string,\n options: UseIntegrationActionOptions,\n): UseMutationResult<TOutput, IntegrationActionError, TInput> {\n return useMutation({\n mutationFn: (input: TInput) =>\n runIntegrationAction<TInput, TOutput>(\n integrationSlug,\n actionName,\n input,\n options,\n ),\n });\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Result shape of an integration's check-connection server action
|
|
3
|
-
* (the standard
|
|
3
|
+
* (the standard server-action envelope).
|
|
4
4
|
*/
|
|
5
5
|
export interface ConnectionCheckResult {
|
|
6
6
|
success: boolean;
|
|
@@ -40,26 +40,6 @@ export interface UseOAuthConnectionStatusOptions {
|
|
|
40
40
|
* connected without a manual refresh.
|
|
41
41
|
*/
|
|
42
42
|
export declare function useOAuthConnectionStatus(accountId: string, options: UseOAuthConnectionStatusOptions): {
|
|
43
|
-
oauthJustCompleted: boolean;
|
|
44
|
-
oauthError: string | null;
|
|
45
|
-
data: {
|
|
46
|
-
isConnected: boolean;
|
|
47
|
-
error: string | undefined;
|
|
48
|
-
obtainedAt?: undefined;
|
|
49
|
-
} | {
|
|
50
|
-
isConnected: boolean;
|
|
51
|
-
obtainedAt: string | null | undefined;
|
|
52
|
-
error?: undefined;
|
|
53
|
-
};
|
|
54
|
-
error: Error;
|
|
55
|
-
isError: true;
|
|
56
|
-
isPending: false;
|
|
57
|
-
isLoading: false;
|
|
58
|
-
isLoadingError: false;
|
|
59
|
-
isRefetchError: true;
|
|
60
|
-
isSuccess: false;
|
|
61
|
-
isPlaceholderData: false;
|
|
62
|
-
status: "error";
|
|
63
43
|
dataUpdatedAt: number;
|
|
64
44
|
errorUpdatedAt: number;
|
|
65
45
|
failureCount: number;
|
|
@@ -78,32 +58,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
78
58
|
error: string | undefined;
|
|
79
59
|
obtainedAt?: undefined;
|
|
80
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
81
62
|
isConnected: boolean;
|
|
82
63
|
obtainedAt: string | null | undefined;
|
|
83
|
-
error?: undefined;
|
|
84
64
|
}, Error>>;
|
|
85
65
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
data: {
|
|
90
|
-
isConnected: boolean;
|
|
91
|
-
error: string | undefined;
|
|
92
|
-
obtainedAt?: undefined;
|
|
93
|
-
} | {
|
|
94
|
-
isConnected: boolean;
|
|
95
|
-
obtainedAt: string | null | undefined;
|
|
96
|
-
error?: undefined;
|
|
97
|
-
};
|
|
98
|
-
error: null;
|
|
99
|
-
isError: false;
|
|
66
|
+
data: undefined;
|
|
67
|
+
error: Error;
|
|
68
|
+
isError: true;
|
|
100
69
|
isPending: false;
|
|
101
70
|
isLoading: false;
|
|
102
|
-
isLoadingError:
|
|
71
|
+
isLoadingError: true;
|
|
103
72
|
isRefetchError: false;
|
|
104
|
-
isSuccess:
|
|
73
|
+
isSuccess: false;
|
|
105
74
|
isPlaceholderData: false;
|
|
106
|
-
status:
|
|
75
|
+
status: 'error';
|
|
76
|
+
oauthJustCompleted: boolean;
|
|
77
|
+
oauthError: string | null;
|
|
78
|
+
} | {
|
|
107
79
|
dataUpdatedAt: number;
|
|
108
80
|
errorUpdatedAt: number;
|
|
109
81
|
failureCount: number;
|
|
@@ -122,24 +94,24 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
122
94
|
error: string | undefined;
|
|
123
95
|
obtainedAt?: undefined;
|
|
124
96
|
} | {
|
|
97
|
+
error?: undefined;
|
|
125
98
|
isConnected: boolean;
|
|
126
99
|
obtainedAt: string | null | undefined;
|
|
127
|
-
error?: undefined;
|
|
128
100
|
}, Error>>;
|
|
129
101
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
130
|
-
} | {
|
|
131
|
-
oauthJustCompleted: boolean;
|
|
132
|
-
oauthError: string | null;
|
|
133
102
|
data: undefined;
|
|
134
|
-
error:
|
|
135
|
-
isError:
|
|
136
|
-
isPending:
|
|
137
|
-
isLoading:
|
|
138
|
-
isLoadingError:
|
|
103
|
+
error: null;
|
|
104
|
+
isError: false;
|
|
105
|
+
isPending: true;
|
|
106
|
+
isLoading: true;
|
|
107
|
+
isLoadingError: false;
|
|
139
108
|
isRefetchError: false;
|
|
140
109
|
isSuccess: false;
|
|
141
110
|
isPlaceholderData: false;
|
|
142
|
-
status:
|
|
111
|
+
status: 'pending';
|
|
112
|
+
oauthJustCompleted: boolean;
|
|
113
|
+
oauthError: string | null;
|
|
114
|
+
} | {
|
|
143
115
|
dataUpdatedAt: number;
|
|
144
116
|
errorUpdatedAt: number;
|
|
145
117
|
failureCount: number;
|
|
@@ -148,6 +120,7 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
148
120
|
isFetched: boolean;
|
|
149
121
|
isFetchedAfterMount: boolean;
|
|
150
122
|
isFetching: boolean;
|
|
123
|
+
isLoading: boolean;
|
|
151
124
|
isInitialLoading: boolean;
|
|
152
125
|
isPaused: boolean;
|
|
153
126
|
isRefetching: boolean;
|
|
@@ -158,24 +131,23 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
158
131
|
error: string | undefined;
|
|
159
132
|
obtainedAt?: undefined;
|
|
160
133
|
} | {
|
|
134
|
+
error?: undefined;
|
|
161
135
|
isConnected: boolean;
|
|
162
136
|
obtainedAt: string | null | undefined;
|
|
163
|
-
error?: undefined;
|
|
164
137
|
}, Error>>;
|
|
165
138
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
166
|
-
} | {
|
|
167
|
-
oauthJustCompleted: boolean;
|
|
168
|
-
oauthError: string | null;
|
|
169
139
|
data: undefined;
|
|
170
140
|
error: null;
|
|
171
141
|
isError: false;
|
|
172
142
|
isPending: true;
|
|
173
|
-
isLoading: true;
|
|
174
143
|
isLoadingError: false;
|
|
175
144
|
isRefetchError: false;
|
|
176
145
|
isSuccess: false;
|
|
177
146
|
isPlaceholderData: false;
|
|
178
|
-
status:
|
|
147
|
+
status: 'pending';
|
|
148
|
+
oauthJustCompleted: boolean;
|
|
149
|
+
oauthError: string | null;
|
|
150
|
+
} | {
|
|
179
151
|
dataUpdatedAt: number;
|
|
180
152
|
errorUpdatedAt: number;
|
|
181
153
|
failureCount: number;
|
|
@@ -194,23 +166,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
194
166
|
error: string | undefined;
|
|
195
167
|
obtainedAt?: undefined;
|
|
196
168
|
} | {
|
|
169
|
+
error?: undefined;
|
|
197
170
|
isConnected: boolean;
|
|
198
171
|
obtainedAt: string | null | undefined;
|
|
199
|
-
error?: undefined;
|
|
200
172
|
}, Error>>;
|
|
201
173
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
174
|
+
data: {
|
|
175
|
+
isConnected: boolean;
|
|
176
|
+
error: string | undefined;
|
|
177
|
+
obtainedAt?: undefined;
|
|
178
|
+
} | {
|
|
179
|
+
error?: undefined;
|
|
180
|
+
isConnected: boolean;
|
|
181
|
+
obtainedAt: string | null | undefined;
|
|
182
|
+
};
|
|
207
183
|
isError: false;
|
|
208
|
-
|
|
184
|
+
error: null;
|
|
185
|
+
isPending: false;
|
|
186
|
+
isLoading: false;
|
|
209
187
|
isLoadingError: false;
|
|
210
188
|
isRefetchError: false;
|
|
211
|
-
isSuccess:
|
|
212
|
-
isPlaceholderData:
|
|
213
|
-
status:
|
|
189
|
+
isSuccess: true;
|
|
190
|
+
isPlaceholderData: true;
|
|
191
|
+
status: 'success';
|
|
192
|
+
oauthJustCompleted: boolean;
|
|
193
|
+
oauthError: string | null;
|
|
194
|
+
} | {
|
|
214
195
|
dataUpdatedAt: number;
|
|
215
196
|
errorUpdatedAt: number;
|
|
216
197
|
failureCount: number;
|
|
@@ -219,7 +200,6 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
219
200
|
isFetched: boolean;
|
|
220
201
|
isFetchedAfterMount: boolean;
|
|
221
202
|
isFetching: boolean;
|
|
222
|
-
isLoading: boolean;
|
|
223
203
|
isInitialLoading: boolean;
|
|
224
204
|
isPaused: boolean;
|
|
225
205
|
isRefetching: boolean;
|
|
@@ -230,32 +210,32 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
230
210
|
error: string | undefined;
|
|
231
211
|
obtainedAt?: undefined;
|
|
232
212
|
} | {
|
|
213
|
+
error?: undefined;
|
|
233
214
|
isConnected: boolean;
|
|
234
215
|
obtainedAt: string | null | undefined;
|
|
235
|
-
error?: undefined;
|
|
236
216
|
}, Error>>;
|
|
237
217
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
238
|
-
} | {
|
|
239
|
-
oauthJustCompleted: boolean;
|
|
240
|
-
oauthError: string | null;
|
|
241
218
|
data: {
|
|
242
219
|
isConnected: boolean;
|
|
243
220
|
error: string | undefined;
|
|
244
221
|
obtainedAt?: undefined;
|
|
245
222
|
} | {
|
|
223
|
+
error?: undefined;
|
|
246
224
|
isConnected: boolean;
|
|
247
225
|
obtainedAt: string | null | undefined;
|
|
248
|
-
error?: undefined;
|
|
249
226
|
};
|
|
250
|
-
|
|
251
|
-
|
|
227
|
+
error: Error;
|
|
228
|
+
isError: true;
|
|
252
229
|
isPending: false;
|
|
253
230
|
isLoading: false;
|
|
254
231
|
isLoadingError: false;
|
|
255
|
-
isRefetchError:
|
|
256
|
-
isSuccess:
|
|
257
|
-
isPlaceholderData:
|
|
258
|
-
status:
|
|
232
|
+
isRefetchError: true;
|
|
233
|
+
isSuccess: false;
|
|
234
|
+
isPlaceholderData: false;
|
|
235
|
+
status: 'error';
|
|
236
|
+
oauthJustCompleted: boolean;
|
|
237
|
+
oauthError: string | null;
|
|
238
|
+
} | {
|
|
259
239
|
dataUpdatedAt: number;
|
|
260
240
|
errorUpdatedAt: number;
|
|
261
241
|
failureCount: number;
|
|
@@ -274,11 +254,31 @@ export declare function useOAuthConnectionStatus(accountId: string, options: Use
|
|
|
274
254
|
error: string | undefined;
|
|
275
255
|
obtainedAt?: undefined;
|
|
276
256
|
} | {
|
|
257
|
+
error?: undefined;
|
|
277
258
|
isConnected: boolean;
|
|
278
259
|
obtainedAt: string | null | undefined;
|
|
279
|
-
error?: undefined;
|
|
280
260
|
}, Error>>;
|
|
281
261
|
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
262
|
+
data: {
|
|
263
|
+
isConnected: boolean;
|
|
264
|
+
error: string | undefined;
|
|
265
|
+
obtainedAt?: undefined;
|
|
266
|
+
} | {
|
|
267
|
+
error?: undefined;
|
|
268
|
+
isConnected: boolean;
|
|
269
|
+
obtainedAt: string | null | undefined;
|
|
270
|
+
};
|
|
271
|
+
error: null;
|
|
272
|
+
isError: false;
|
|
273
|
+
isPending: false;
|
|
274
|
+
isLoading: false;
|
|
275
|
+
isLoadingError: false;
|
|
276
|
+
isRefetchError: false;
|
|
277
|
+
isSuccess: true;
|
|
278
|
+
isPlaceholderData: false;
|
|
279
|
+
status: 'success';
|
|
280
|
+
oauthJustCompleted: boolean;
|
|
281
|
+
oauthError: string | null;
|
|
282
282
|
};
|
|
283
283
|
/**
|
|
284
284
|
* Resolves the gating booleans every OAuth-backed dashboard widget needs:
|
|
@@ -42,14 +42,18 @@ export function useOAuthConnectionStatus(accountId, options) {
|
|
|
42
42
|
refetchOnWindowFocus: false,
|
|
43
43
|
});
|
|
44
44
|
const errorParamName = errorParam === null || errorParam === void 0 ? void 0 : errorParam.name;
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
45
|
+
const returnedWithError = oauthError !== null;
|
|
46
|
+
// Justified useEffect: after the OAuth redirect lands back on this page —
|
|
47
|
+
// with `?oauth_success=true` OR with the integration's error param — we
|
|
48
|
+
// must (1) strip the transient query params from the URL, so a reload does
|
|
49
|
+
// not re-announce a stale success or re-show a stale error, and (2) on
|
|
50
|
+
// success invalidate the cached connection status so widgets flip to
|
|
51
|
+
// connected without a manual refresh. Both are post-render side effects
|
|
52
|
+
// on external systems (history API + query cache), not derivable during
|
|
53
|
+
// render. The error itself stays in state for this render, so the alert
|
|
54
|
+
// still shows once.
|
|
51
55
|
useEffect(() => {
|
|
52
|
-
if (!oauthJustCompleted)
|
|
56
|
+
if (!oauthJustCompleted && !returnedWithError)
|
|
53
57
|
return;
|
|
54
58
|
const url = new URL(window.location.href);
|
|
55
59
|
url.searchParams.delete('oauth_success');
|
|
@@ -57,11 +61,14 @@ export function useOAuthConnectionStatus(accountId, options) {
|
|
|
57
61
|
url.searchParams.delete(errorParamName);
|
|
58
62
|
}
|
|
59
63
|
window.history.replaceState({}, '', url.pathname + url.search);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
if (oauthJustCompleted) {
|
|
65
|
+
queryClient.invalidateQueries({
|
|
66
|
+
queryKey: ['integration-data', accountId, connectionKey],
|
|
67
|
+
});
|
|
68
|
+
}
|
|
63
69
|
}, [
|
|
64
70
|
oauthJustCompleted,
|
|
71
|
+
returnedWithError,
|
|
65
72
|
accountId,
|
|
66
73
|
connectionKey,
|
|
67
74
|
errorParamName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE5C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAuCjE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CACtC,SAAiB,EACjB,OAAwC;IAExC,MAAM,EACJ,aAAa,EACb,eAAe,EACf,OAAO,GAAG,IAAI,EACd,UAAU,GACX,GAAG,OAAO,CAAC;IACZ,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,yEAAyE;IACzE,0EAA0E;IAC1E,MAAM,CAAC,kBAAkB,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE;QACzC,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,KAAK,CAAC;QAChD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC3D,OAAO,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,KAAK,MAAM,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,UAAU,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE;QAChD,IAAI,CAAC,UAAU,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9D,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,CAC3D,UAAU,CAAC,IAAI,CAChB,CAAC;QACF,OAAO,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,MAAM,KAAK,GAAG,QAAQ,CAAC;QACrB,QAAQ,EAAE,CAAC,kBAAkB,EAAE,SAAS,EAAE,aAAa,CAAC;QACxD,OAAO,EAAE,KAAK,IAAI,EAAE;;YAClB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC;YAEpD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC;YACrD,CAAC;YAED,OAAO;gBACL,WAAW,EAAE,IAAI;gBACjB,UAAU,EAAE,MAAA,MAAM,CAAC,IAAI,0CAAE,UAAU;aACpC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,OAAO,IAAI,CAAC,CAAC,SAAS;QAC/B,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,IAAI;QACxB,oBAAoB,EAAE,KAAK;KAC5B,CAAC,CAAC;IAEH,MAAM,cAAc,GAAG,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,IAAI,CAAC;IAExC,
|
|
1
|
+
{"version":3,"file":"use-oauth-connection-status.js","sourceRoot":"","sources":["../../src/hooks/use-oauth-connection-status.ts"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport { useEffect, useState } from 'react';\n\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\n\n/**\n * Result shape of an integration's check-connection server action\n * (the standard server-action envelope).\n */\nexport interface ConnectionCheckResult {\n success: boolean;\n data?: { obtainedAt?: string | null } | null;\n error?: string;\n}\n\nexport type ConnectionCheckAction = (input: {\n accountId: string;\n}) => Promise<ConnectionCheckResult>;\n\n/**\n * Optional OAuth failure query param an integration's connect route may\n * append on redirect (e.g. dropbox_error=credentials_not_configured).\n */\nexport interface OAuthErrorParam {\n name: string;\n /** Map the raw param value to a user-facing message; return null to ignore. */\n toMessage: (value: string) => string | null;\n}\n\nexport interface UseOAuthConnectionStatusOptions {\n /**\n * Cache key segment, by convention `<slug>-connection`. The full query key\n * is ['integration-data', accountId, connectionKey] so all widgets and the\n * activation form share one deduped request per dashboard load.\n */\n connectionKey: string;\n checkConnection: ConnectionCheckAction;\n /** Gate the query (e.g. on integration-active state). Defaults to true. */\n enabled?: boolean;\n errorParam?: OAuthErrorParam;\n}\n\n/**\n * Shared OAuth connection-status hook. Detects the `oauth_success=true`\n * return from an integration's connect route, strips the transient params\n * from the URL, and invalidates the cached status so dependent UI flips to\n * connected without a manual refresh.\n */\nexport function useOAuthConnectionStatus(\n accountId: string,\n options: UseOAuthConnectionStatusOptions,\n) {\n const {\n connectionKey,\n checkConnection,\n enabled = true,\n errorParam,\n } = options;\n const queryClient = useQueryClient();\n\n // Read the OAuth return params synchronously on mount (not in an effect)\n // so the first render already knows whether we just came back from OAuth.\n const [oauthJustCompleted] = useState(() => {\n if (typeof window === 'undefined') return false;\n const params = new URLSearchParams(window.location.search);\n return params.get('oauth_success') === 'true';\n });\n\n const [oauthError] = useState<string | null>(() => {\n if (!errorParam || typeof window === 'undefined') return null;\n const value = new URLSearchParams(window.location.search).get(\n errorParam.name,\n );\n return value ? errorParam.toMessage(value) : null;\n });\n\n const query = useQuery({\n queryKey: ['integration-data', accountId, connectionKey],\n queryFn: async () => {\n const result = await checkConnection({ accountId });\n\n if (!result.success) {\n return { isConnected: false, error: result.error };\n }\n\n return {\n isConnected: true,\n obtainedAt: result.data?.obtainedAt,\n };\n },\n enabled: enabled && !!accountId,\n staleTime: 5 * 60 * 1000,\n refetchOnWindowFocus: false,\n });\n\n const errorParamName = errorParam?.name;\n\n const returnedWithError = oauthError !== null;\n\n // Justified useEffect: after the OAuth redirect lands back on this page —\n // with `?oauth_success=true` OR with the integration's error param — we\n // must (1) strip the transient query params from the URL, so a reload does\n // not re-announce a stale success or re-show a stale error, and (2) on\n // success invalidate the cached connection status so widgets flip to\n // connected without a manual refresh. Both are post-render side effects\n // on external systems (history API + query cache), not derivable during\n // render. The error itself stays in state for this render, so the alert\n // still shows once.\n useEffect(() => {\n if (!oauthJustCompleted && !returnedWithError) return;\n\n const url = new URL(window.location.href);\n url.searchParams.delete('oauth_success');\n\n if (errorParamName) {\n url.searchParams.delete(errorParamName);\n }\n\n window.history.replaceState({}, '', url.pathname + url.search);\n\n if (oauthJustCompleted) {\n queryClient.invalidateQueries({\n queryKey: ['integration-data', accountId, connectionKey],\n });\n }\n }, [\n oauthJustCompleted,\n returnedWithError,\n accountId,\n connectionKey,\n errorParamName,\n queryClient,\n ]);\n\n return { ...query, oauthJustCompleted, oauthError };\n}\n\n/**\n * Resolves the gating booleans every OAuth-backed dashboard widget needs:\n *\n * - `enabled` — the integration is active and an account id is present.\n * - `isConnected` — a live OAuth connection exists (access token).\n * - `dataEnabled` — safe to fire the widget's data query (active + connected).\n * - `connectionPending` — still resolving the connection status.\n * - `connectionError` — the connection check itself failed (network/5xx).\n * Distinct from `notConnected`: a transient failure should surface as an\n * error, not as a misleading \"connect X\" prompt.\n * - `notConnected` — activated, resolved without error, but no OAuth\n * connection. The widget should render its \"connect X\" inactive state.\n *\n * The underlying connection check shares one query key across all widgets,\n * so react-query dedupes it to a single request per dashboard load.\n */\nexport function useOAuthWidgetGate(\n accountId: string | undefined,\n integrationActive: boolean,\n options: Pick<\n UseOAuthConnectionStatusOptions,\n 'connectionKey' | 'checkConnection'\n >,\n) {\n const enabled = integrationActive && !!accountId;\n const query = useOAuthConnectionStatus(accountId ?? '', {\n ...options,\n enabled,\n });\n\n const connectionPending = enabled && query.isPending;\n const connectionError = enabled && query.isError;\n const isConnected = query.data?.isConnected === true;\n const dataEnabled = enabled && isConnected;\n // Only treat as \"not connected\" when the check resolved successfully with\n // no token — a query error is an error state, not a disconnected account.\n const notConnected =\n enabled && !connectionPending && !connectionError && !isConnected;\n\n return {\n enabled,\n isConnected,\n connectionPending,\n connectionError,\n dataEnabled,\n notConnected,\n };\n}\n"]}
|