@ekanos/sdk 0.1.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/LICENSE +21 -0
- package/README.md +78 -0
- package/dist/components/base-activation-dialog.d.ts +41 -0
- package/dist/components/base-activation-dialog.js +41 -0
- package/dist/components/base-activation-dialog.js.map +1 -0
- package/dist/components/base-activation-form.d.ts +36 -0
- package/dist/components/base-activation-form.js +21 -0
- package/dist/components/base-activation-form.js.map +1 -0
- package/dist/components/base-marketplace-tile.d.ts +34 -0
- package/dist/components/base-marketplace-tile.js +40 -0
- package/dist/components/base-marketplace-tile.js.map +1 -0
- package/dist/components/current-return-path.d.ts +19 -0
- package/dist/components/current-return-path.js +26 -0
- package/dist/components/current-return-path.js.map +1 -0
- package/dist/components/index.d.ts +40 -0
- package/dist/components/index.js +38 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/oauth-activation-form.d.ts +50 -0
- package/dist/components/oauth-activation-form.js +119 -0
- package/dist/components/oauth-activation-form.js.map +1 -0
- package/dist/components/widgets/ai-prompt-chip.d.ts +31 -0
- package/dist/components/widgets/ai-prompt-chip.js +38 -0
- package/dist/components/widgets/ai-prompt-chip.js.map +1 -0
- package/dist/components/widgets/ask-icon.d.ts +3 -0
- package/dist/components/widgets/ask-icon.js +5 -0
- package/dist/components/widgets/ask-icon.js.map +1 -0
- package/dist/components/widgets/use-animated-height.d.ts +12 -0
- package/dist/components/widgets/use-animated-height.js +39 -0
- package/dist/components/widgets/use-animated-height.js.map +1 -0
- package/dist/components/widgets/widget-ask-bar.d.ts +18 -0
- package/dist/components/widgets/widget-ask-bar.js +62 -0
- package/dist/components/widgets/widget-ask-bar.js.map +1 -0
- package/dist/components/widgets/widget-context.d.ts +38 -0
- package/dist/components/widgets/widget-context.js +4 -0
- package/dist/components/widgets/widget-context.js.map +1 -0
- package/dist/components/widgets/widget-state-components.d.ts +5 -0
- package/dist/components/widgets/widget-state-components.js +19 -0
- package/dist/components/widgets/widget-state-components.js.map +1 -0
- package/dist/components/widgets/widget.d.ts +91 -0
- package/dist/components/widgets/widget.js +205 -0
- package/dist/components/widgets/widget.js.map +1 -0
- package/dist/context/egress.d.ts +30 -0
- package/dist/context/egress.js +81 -0
- package/dist/context/egress.js.map +1 -0
- package/dist/context/errors.d.ts +57 -0
- package/dist/context/errors.js +78 -0
- package/dist/context/errors.js.map +1 -0
- package/dist/context/index.d.ts +19 -0
- package/dist/context/index.js +20 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/require-context.d.ts +26 -0
- package/dist/context/require-context.js +36 -0
- package/dist/context/require-context.js.map +1 -0
- package/dist/context/storage-validation.d.ts +71 -0
- package/dist/context/storage-validation.js +132 -0
- package/dist/context/storage-validation.js.map +1 -0
- package/dist/context/types.d.ts +11 -0
- package/dist/context/types.js +2 -0
- package/dist/context/types.js.map +1 -0
- package/dist/hooks/activation-actions-context.d.ts +80 -0
- package/dist/hooks/activation-actions-context.js +31 -0
- package/dist/hooks/activation-actions-context.js.map +1 -0
- package/dist/hooks/ask-assistant-context.d.ts +32 -0
- package/dist/hooks/ask-assistant-context.js +56 -0
- package/dist/hooks/ask-assistant-context.js.map +1 -0
- package/dist/hooks/fetch-integration-storage.d.ts +48 -0
- package/dist/hooks/fetch-integration-storage.js +68 -0
- package/dist/hooks/fetch-integration-storage.js.map +1 -0
- package/dist/hooks/index.d.ts +28 -0
- package/dist/hooks/index.js +32 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/use-activate-integration.d.ts +15 -0
- package/dist/hooks/use-activate-integration.js +61 -0
- package/dist/hooks/use-activate-integration.js.map +1 -0
- package/dist/hooks/use-oauth-connection-status.d.ts +360 -0
- package/dist/hooks/use-oauth-connection-status.js +108 -0
- package/dist/hooks/use-oauth-connection-status.js.map +1 -0
- package/dist/hooks/use-widget-events.d.ts +20 -0
- package/dist/hooks/use-widget-events.js +51 -0
- package/dist/hooks/use-widget-events.js.map +1 -0
- package/dist/hooks/widget-event-context.d.ts +22 -0
- package/dist/hooks/widget-event-context.js +75 -0
- package/dist/hooks/widget-event-context.js.map +1 -0
- package/dist/hooks/widget-event-types.d.ts +55 -0
- package/dist/hooks/widget-event-types.js +9 -0
- package/dist/hooks/widget-event-types.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/cron.d.ts +29 -0
- package/dist/integration/cron.js +102 -0
- package/dist/integration/cron.js.map +1 -0
- package/dist/integration/define-integration.d.ts +28 -0
- package/dist/integration/define-integration.js +41 -0
- package/dist/integration/define-integration.js.map +1 -0
- package/dist/integration/event-surfaces.d.ts +15 -0
- package/dist/integration/event-surfaces.js +45 -0
- package/dist/integration/event-surfaces.js.map +1 -0
- package/dist/integration/index.d.ts +20 -0
- package/dist/integration/index.js +23 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/types.d.ts +7 -0
- package/dist/integration/types.js +2 -0
- package/dist/integration/types.js.map +1 -0
- package/dist/mcp/guards.d.ts +40 -0
- package/dist/mcp/guards.js +99 -0
- package/dist/mcp/guards.js.map +1 -0
- package/dist/mcp/index.d.ts +22 -0
- package/dist/mcp/index.js +22 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/ownership.d.ts +74 -0
- package/dist/mcp/ownership.js +83 -0
- package/dist/mcp/ownership.js.map +1 -0
- package/dist/mcp/types.d.ts +145 -0
- package/dist/mcp/types.js +2 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/invoke.d.ts +77 -0
- package/dist/testing/invoke.js +83 -0
- package/dist/testing/invoke.js.map +1 -0
- package/dist/testing/mock-context.d.ts +79 -0
- package/dist/testing/mock-context.js +206 -0
- package/dist/testing/mock-context.js.map +1 -0
- package/dist/types/icon.d.ts +12 -0
- package/dist/types/icon.js +2 -0
- package/dist/types/icon.js.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/integration.d.ts +258 -0
- package/dist/types/integration.js +2 -0
- package/dist/types/integration.js.map +1 -0
- package/dist/types/widget-ask-context.d.ts +36 -0
- package/dist/types/widget-ask-context.js +2 -0
- package/dist/types/widget-ask-context.js.map +1 -0
- package/dist/types/workspace-target.d.ts +33 -0
- package/dist/types/workspace-target.js +2 -0
- package/dist/types/workspace-target.js.map +1 -0
- package/package.json +116 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch-integration-storage.js","sourceRoot":"","sources":["../../src/hooks/fetch-integration-storage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,oDAAoD;AACpD,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAgBhC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CACnC,eAAuB,EACvB,OAEC;;IAED,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;KAC7B,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAA,OAAO,CAAC,OAAO,mCAAI,mBAAmB,CAAC;IAEpD,OAAO,GAAG,IAAI,IAAI,eAAe,YAAY,KAAK,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,eAAuB,EACvB,OAAuC;;IAEvC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oCAAoC,CAChF,CAAC;IACJ,CAAC;IAED,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAC/C,MAAM,IAAI,KAAK,CACb,4BAA4B,eAAe,oBAAoB,OAAO,CAAC,IAAI,CAAC,MAAM,SAAS;YACzF,gCAAgC,oBAAoB,+BAA+B,CACtF,CAAC;IACJ,CAAC;IAED,MAAM,GAAG,GAAG,qBAAqB,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IAE5D,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC,GAAG,EAAE;QAC3C,MAAM,EAAE,MAAA,OAAO,CAAC,MAAM,mCAAI,IAAI;QAC9B,OAAO,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE;KACxC,CAAC,CAAC;IAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CACb,mBAAmB,eAAe,cAAc,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,IAAI,CAC3F,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IAE/C,IACE,OAAO,KAAK,IAAI;QAChB,OAAO,OAAO,KAAK,QAAQ;QAC3B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,QAAQ,eAAe,gDAAgD,CACxE,CAAC;IACJ,CAAC;IAED,OAAO,OAAkC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * The browser half of the generic storage bridge.\n *\n * `ctx.storage` is a server capability and widgets are client components, so\n * a saved setting has to round-trip through HTTP. The host serves that\n * round-trip once, generically, at\n * `GET /api/integrations/[slug]/storage?accountId=…&keys=a,b`; this is the\n * client that calls it, so an integration never hand-writes the URL, the\n * query string, or the not-ok handling.\n *\n * Only keys whose declaration opted in with `clientReadable: true` come back.\n * A key that did not opt in, was never declared, or simply has no stored row\n * is ABSENT from the result — all three are indistinguishable by design, so\n * callers must treat a missing key as \"no value\", never as an error.\n *\n * Values are returned raw and unvalidated: the integration owns the shape of\n * its own storage, so it validates with the same zod schemas it declared.\n * See the Open-Meteo example's `fetchWeatherSettings` for the pattern.\n */\n\n/** Mirrors the host route's own per-request cap. */\nconst MAX_KEYS_PER_REQUEST = 20;\n\nexport interface FetchIntegrationStorageOptions {\n /** The account whose storage to read. Must be one the caller belongs to. */\n accountId: string;\n /** Declared, `clientReadable` account-scope keys, e.g. `['settings/units']`. */\n keys: readonly string[];\n signal?: AbortSignal;\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 storage-route URL for one integration. Exported because a\n * harness or a test double needs to recognize the exact URL this client will\n * request.\n */\nexport function integrationStorageUrl(\n integrationSlug: string,\n options: Pick<FetchIntegrationStorageOptions, 'accountId' | 'keys'> & {\n baseUrl?: string;\n },\n): string {\n const query = new URLSearchParams({\n accountId: options.accountId,\n keys: options.keys.join(','),\n });\n\n const base = options.baseUrl ?? '/api/integrations';\n\n return `${base}/${integrationSlug}/storage?${query}`;\n}\n\n/**\n * Reads declared, client-readable account-scope storage for one integration.\n *\n * Throws when the request itself fails — a 403 (not signed in, not a member,\n * not activated, or no such integration, all deliberately indistinguishable)\n * or a transport error. It does NOT throw for a key that came back absent.\n */\nexport async function fetchIntegrationStorage(\n integrationSlug: string,\n options: FetchIntegrationStorageOptions,\n): Promise<Record<string, unknown>> {\n if (options.keys.length === 0) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') needs at least one storage key.`,\n );\n }\n\n if (options.keys.length > MAX_KEYS_PER_REQUEST) {\n throw new Error(\n `fetchIntegrationStorage('${integrationSlug}') was asked for ${options.keys.length} keys; ` +\n `the host route reads at most ${MAX_KEYS_PER_REQUEST} per request. Split the read.`,\n );\n }\n\n const url = integrationStorageUrl(integrationSlug, options);\n\n const response = await globalThis.fetch(url, {\n signal: options.signal ?? null,\n headers: { accept: 'application/json' },\n });\n\n if (!response.ok) {\n throw new Error(\n `Could not read '${integrationSlug}' storage (${response.status} ${response.statusText}).`,\n );\n }\n\n const payload: unknown = await response.json();\n\n if (\n payload === null ||\n typeof payload !== 'object' ||\n Array.isArray(payload)\n ) {\n throw new Error(\n `The '${integrationSlug}' storage route returned a non-object payload.`,\n );\n }\n\n return payload as Record<string, unknown>;\n}\n"]}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/hooks — client-side hooks.
|
|
3
|
+
*
|
|
4
|
+
* Import from client components only. The SDK OWNS these implementations;
|
|
5
|
+
* `@kit/integrations-core/hooks` re-exports them for first-party code, so
|
|
6
|
+
* there is one source of truth with two consumption paths
|
|
7
|
+
* (DEVELOPER_EXPERIENCE_PLAN.md §5.1).
|
|
8
|
+
*
|
|
9
|
+
* Server seam: nothing here imports a server module. The activation hooks
|
|
10
|
+
* reach the host's `'use server'` actions ONLY through the
|
|
11
|
+
* `IntegrationActivationProvider` injection seam
|
|
12
|
+
* (activation-actions-context.tsx) — that is what keeps this entrypoint free
|
|
13
|
+
* of `server-only` in a partner's module graph.
|
|
14
|
+
*
|
|
15
|
+
* Surface discipline: additions require an entry in
|
|
16
|
+
* docs/devex/sdk-export-map.md ("`@ekanos/sdk/hooks`").
|
|
17
|
+
*/
|
|
18
|
+
export { useActivateIntegration, useDeactivateIntegration, } from './use-activate-integration.js';
|
|
19
|
+
export { IntegrationActivationProvider } from './activation-actions-context.js';
|
|
20
|
+
export type { ActivateIntegrationAction, ActivateIntegrationInput, DeactivateIntegrationAction, DeactivateIntegrationInput, IntegrationActionResult, IntegrationActivationActions, } from './activation-actions-context.js';
|
|
21
|
+
export { fetchIntegrationStorage, integrationStorageUrl, } from './fetch-integration-storage.js';
|
|
22
|
+
export type { FetchIntegrationStorageOptions } from './fetch-integration-storage.js';
|
|
23
|
+
export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-connection-status.js';
|
|
24
|
+
export type { ConnectionCheckAction, ConnectionCheckResult, OAuthErrorParam, UseOAuthConnectionStatusOptions, } from './use-oauth-connection-status.js';
|
|
25
|
+
export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
|
|
26
|
+
export { useWidgetEvents } from './use-widget-events.js';
|
|
27
|
+
export type { PublishEventInput, WidgetEvent, WidgetEventFilter, WidgetEventSubscriber, } from './widget-event-types.js';
|
|
28
|
+
export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ekanos/sdk/hooks — client-side hooks.
|
|
3
|
+
*
|
|
4
|
+
* Import from client components only. The SDK OWNS these implementations;
|
|
5
|
+
* `@kit/integrations-core/hooks` re-exports them for first-party code, so
|
|
6
|
+
* there is one source of truth with two consumption paths
|
|
7
|
+
* (DEVELOPER_EXPERIENCE_PLAN.md §5.1).
|
|
8
|
+
*
|
|
9
|
+
* Server seam: nothing here imports a server module. The activation hooks
|
|
10
|
+
* reach the host's `'use server'` actions ONLY through the
|
|
11
|
+
* `IntegrationActivationProvider` injection seam
|
|
12
|
+
* (activation-actions-context.tsx) — that is what keeps this entrypoint free
|
|
13
|
+
* of `server-only` in a partner's module graph.
|
|
14
|
+
*
|
|
15
|
+
* Surface discipline: additions require an entry in
|
|
16
|
+
* docs/devex/sdk-export-map.md ("`@ekanos/sdk/hooks`").
|
|
17
|
+
*/
|
|
18
|
+
// Activation (host-injected server actions)
|
|
19
|
+
export { useActivateIntegration, useDeactivateIntegration, } from './use-activate-integration.js';
|
|
20
|
+
export { IntegrationActivationProvider } from './activation-actions-context.js';
|
|
21
|
+
// Generic client-side storage bridge (host route:
|
|
22
|
+
// GET /api/integrations/[slug]/storage). Serves only keys declared
|
|
23
|
+
// `clientReadable: true`; a key that did not opt in reads as absent.
|
|
24
|
+
export { fetchIntegrationStorage, integrationStorageUrl, } from './fetch-integration-storage.js';
|
|
25
|
+
// OAuth connection status
|
|
26
|
+
export { useOAuthConnectionStatus, useOAuthWidgetGate, } from './use-oauth-connection-status.js';
|
|
27
|
+
// Widget Event Bus
|
|
28
|
+
export { WidgetEventProvider, useWidgetEventBus } from './widget-event-context.js';
|
|
29
|
+
export { useWidgetEvents } from './use-widget-events.js';
|
|
30
|
+
// Widget Ask-Bar Bridge
|
|
31
|
+
export { AskAssistantProvider, useAskAssistant, useRegisterAskAssistant, } from './ask-assistant-context.js';
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,4CAA4C;AAC5C,OAAO,EACL,sBAAsB,EACtB,wBAAwB,GACzB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC;AAU7E,kDAAkD;AAClD,mEAAmE;AACnE,qEAAqE;AACrE,OAAO,EACL,uBAAuB,EACvB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAGrC,0BAA0B;AAC1B,OAAO,EACL,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,+BAA+B,CAAC;AAQvC,mBAAmB;AACnB,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAQtD,wBAAwB;AACxB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,uBAAuB,GACxB,MAAM,yBAAyB,CAAC","sourcesContent":["/**\n * @ekanos/sdk/hooks — client-side hooks.\n *\n * Import from client components only. The SDK OWNS these implementations;\n * `@kit/integrations-core/hooks` re-exports them for first-party code, so\n * there is one source of truth with two consumption paths\n * (DEVELOPER_EXPERIENCE_PLAN.md §5.1).\n *\n * Server seam: nothing here imports a server module. The activation hooks\n * reach the host's `'use server'` actions ONLY through the\n * `IntegrationActivationProvider` injection seam\n * (activation-actions-context.tsx) — that is what keeps this entrypoint free\n * of `server-only` in a partner's module graph.\n *\n * Surface discipline: additions require an entry in\n * docs/devex/sdk-export-map.md (\"`@ekanos/sdk/hooks`\").\n */\n\n// Activation (host-injected server actions)\nexport {\n useActivateIntegration,\n useDeactivateIntegration,\n} from './use-activate-integration';\nexport { IntegrationActivationProvider } from './activation-actions-context';\nexport type {\n ActivateIntegrationAction,\n ActivateIntegrationInput,\n DeactivateIntegrationAction,\n DeactivateIntegrationInput,\n IntegrationActionResult,\n IntegrationActivationActions,\n} from './activation-actions-context';\n\n// Generic client-side storage bridge (host route:\n// GET /api/integrations/[slug]/storage). Serves only keys declared\n// `clientReadable: true`; a key that did not opt in reads as absent.\nexport {\n fetchIntegrationStorage,\n integrationStorageUrl,\n} from './fetch-integration-storage';\nexport type { FetchIntegrationStorageOptions } from './fetch-integration-storage';\n\n// OAuth connection status\nexport {\n useOAuthConnectionStatus,\n useOAuthWidgetGate,\n} from './use-oauth-connection-status';\nexport type {\n ConnectionCheckAction,\n ConnectionCheckResult,\n OAuthErrorParam,\n UseOAuthConnectionStatusOptions,\n} from './use-oauth-connection-status';\n\n// Widget Event Bus\nexport { WidgetEventProvider, useWidgetEventBus } from './widget-event-context';\nexport { useWidgetEvents } from './use-widget-events';\nexport type {\n PublishEventInput,\n WidgetEvent,\n WidgetEventFilter,\n WidgetEventSubscriber,\n} from './widget-event-types';\n\n// Widget Ask-Bar Bridge\nexport {\n AskAssistantProvider,\n useAskAssistant,\n useRegisterAskAssistant,\n} from './ask-assistant-context';\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ActivateIntegrationInput, type DeactivateIntegrationInput } from './activation-actions-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Hook to activate an integration.
|
|
4
|
+
*
|
|
5
|
+
* The actual activation server action is host-injected via
|
|
6
|
+
* `IntegrationActivationProvider` (see activation-actions-context.tsx) — the
|
|
7
|
+
* hook itself never imports host server modules, which is what keeps
|
|
8
|
+
* `@ekanos/sdk/hooks` importable from a plain client (or test) module graph.
|
|
9
|
+
*/
|
|
10
|
+
export declare function useActivateIntegration(): import("@tanstack/react-query").UseMutationResult<import("./activation-actions-context.js").IntegrationActionResult, Error, ActivateIntegrationInput, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Hook to deactivate an integration. Same host-injection contract as
|
|
13
|
+
* `useActivateIntegration`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useDeactivateIntegration(): import("@tanstack/react-query").UseMutationResult<import("./activation-actions-context.js").IntegrationActionResult | undefined, Error, DeactivateIntegrationInput, unknown>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { useIntegrationActivationActions, } from './activation-actions-context.js';
|
|
4
|
+
/**
|
|
5
|
+
* Hook to activate an integration.
|
|
6
|
+
*
|
|
7
|
+
* The actual activation server action is host-injected via
|
|
8
|
+
* `IntegrationActivationProvider` (see activation-actions-context.tsx) — the
|
|
9
|
+
* hook itself never imports host server modules, which is what keeps
|
|
10
|
+
* `@ekanos/sdk/hooks` importable from a plain client (or test) module graph.
|
|
11
|
+
*/
|
|
12
|
+
export function useActivateIntegration() {
|
|
13
|
+
const { activate } = useIntegrationActivationActions();
|
|
14
|
+
const queryClient = useQueryClient();
|
|
15
|
+
return useMutation({
|
|
16
|
+
mutationFn: async (input) => {
|
|
17
|
+
const res = await activate(input);
|
|
18
|
+
if (!res || res.success === false) {
|
|
19
|
+
throw new Error((res === null || res === void 0 ? void 0 : res.errorMessage) || 'Activation failed');
|
|
20
|
+
}
|
|
21
|
+
return res;
|
|
22
|
+
},
|
|
23
|
+
onSuccess: (_data, variables) => {
|
|
24
|
+
// Invalidate ALL integration-status queries for this account
|
|
25
|
+
queryClient.invalidateQueries({
|
|
26
|
+
queryKey: ['integration-status', variables.accountId],
|
|
27
|
+
exact: false, // This will invalidate all queries that start with this key
|
|
28
|
+
});
|
|
29
|
+
// Invalidate any integration data queries
|
|
30
|
+
queryClient.invalidateQueries({
|
|
31
|
+
queryKey: ['integration-data', variables.accountId],
|
|
32
|
+
exact: false,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Hook to deactivate an integration. Same host-injection contract as
|
|
39
|
+
* `useActivateIntegration`.
|
|
40
|
+
*/
|
|
41
|
+
export function useDeactivateIntegration() {
|
|
42
|
+
const { deactivate } = useIntegrationActivationActions();
|
|
43
|
+
const queryClient = useQueryClient();
|
|
44
|
+
return useMutation({
|
|
45
|
+
mutationFn: (input) => deactivate(input),
|
|
46
|
+
onSuccess: (_data, variables) => {
|
|
47
|
+
// Invalidate ALL integration-status queries for this account
|
|
48
|
+
queryClient.invalidateQueries({
|
|
49
|
+
queryKey: ['integration-status', variables.accountId],
|
|
50
|
+
exact: false, // This will invalidate all queries that start with this key
|
|
51
|
+
});
|
|
52
|
+
// Remove integration data queries to prevent stale cache
|
|
53
|
+
// (invalidate keeps stale data; remove clears it so remounted queries start fresh)
|
|
54
|
+
queryClient.removeQueries({
|
|
55
|
+
queryKey: ['integration-data', variables.accountId],
|
|
56
|
+
exact: false,
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=use-activate-integration.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-activate-integration.js","sourceRoot":"","sources":["../../src/hooks/use-activate-integration.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEpE,OAAO,EAGL,+BAA+B,GAChC,MAAM,8BAA8B,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB;IACpC,MAAM,EAAE,QAAQ,EAAE,GAAG,+BAA+B,EAAE,CAAC;IACvD,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,OAAO,WAAW,CAAC;QACjB,UAAU,EAAE,KAAK,EAAE,KAA+B,EAAE,EAAE;YACpD,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,CAAC;YAElC,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;gBAClC,MAAM,IAAI,KAAK,CAAC,CAAA,GAAG,aAAH,GAAG,uBAAH,GAAG,CAAE,YAAY,KAAI,mBAAmB,CAAC,CAAC;YAC5D,CAAC;YAED,OAAO,GAAG,CAAC;QACb,CAAC;QACD,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE;YAC9B,6DAA6D;YAC7D,WAAW,CAAC,iBAAiB,CAAC;gBAC5B,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,CAAC,SAAS,CAAC;gBACrD,KAAK,EAAE,KAAK,EAAE,4DAA4D;aAC3E,CAAC,CAAC;YAEH,0CAA0C;YAC1C,WAAW,CAAC,iBAAiB,CAAC;gBAC5B,QAAQ,EAAE,CAAC,kBAAkB,EAAE,SAAS,CAAC,SAAS,CAAC;gBACnD,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,MAAM,EAAE,UAAU,EAAE,GAAG,+BAA+B,EAAE,CAAC;IACzD,MAAM,WAAW,GAAG,cAAc,EAAE,CAAC;IAErC,OAAO,WAAW,CAAC;QACjB,UAAU,EAAE,CAAC,KAAiC,EAAE,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC;QACpE,SAAS,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE;YAC9B,6DAA6D;YAC7D,WAAW,CAAC,iBAAiB,CAAC;gBAC5B,QAAQ,EAAE,CAAC,oBAAoB,EAAE,SAAS,CAAC,SAAS,CAAC;gBACrD,KAAK,EAAE,KAAK,EAAE,4DAA4D;aAC3E,CAAC,CAAC;YAEH,yDAAyD;YACzD,mFAAmF;YACnF,WAAW,CAAC,aAAa,CAAC;gBACxB,QAAQ,EAAE,CAAC,kBAAkB,EAAE,SAAS,CAAC,SAAS,CAAC;gBACnD,KAAK,EAAE,KAAK;aACb,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CAAC;AACL,CAAC","sourcesContent":["'use client';\n\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\n\nimport {\n type ActivateIntegrationInput,\n type DeactivateIntegrationInput,\n useIntegrationActivationActions,\n} from './activation-actions-context';\n\n/**\n * Hook to activate an integration.\n *\n * The actual activation server action is host-injected via\n * `IntegrationActivationProvider` (see activation-actions-context.tsx) — the\n * hook itself never imports host server modules, which is what keeps\n * `@ekanos/sdk/hooks` importable from a plain client (or test) module graph.\n */\nexport function useActivateIntegration() {\n const { activate } = useIntegrationActivationActions();\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: async (input: ActivateIntegrationInput) => {\n const res = await activate(input);\n\n if (!res || res.success === false) {\n throw new Error(res?.errorMessage || 'Activation failed');\n }\n\n return res;\n },\n onSuccess: (_data, variables) => {\n // Invalidate ALL integration-status queries for this account\n queryClient.invalidateQueries({\n queryKey: ['integration-status', variables.accountId],\n exact: false, // This will invalidate all queries that start with this key\n });\n\n // Invalidate any integration data queries\n queryClient.invalidateQueries({\n queryKey: ['integration-data', variables.accountId],\n exact: false,\n });\n },\n });\n}\n\n/**\n * Hook to deactivate an integration. Same host-injection contract as\n * `useActivateIntegration`.\n */\nexport function useDeactivateIntegration() {\n const { deactivate } = useIntegrationActivationActions();\n const queryClient = useQueryClient();\n\n return useMutation({\n mutationFn: (input: DeactivateIntegrationInput) => deactivate(input),\n onSuccess: (_data, variables) => {\n // Invalidate ALL integration-status queries for this account\n queryClient.invalidateQueries({\n queryKey: ['integration-status', variables.accountId],\n exact: false, // This will invalidate all queries that start with this key\n });\n\n // Remove integration data queries to prevent stale cache\n // (invalidate keeps stale data; remove clears it so remounted queries start fresh)\n queryClient.removeQueries({\n queryKey: ['integration-data', variables.accountId],\n exact: false,\n });\n },\n });\n}\n"]}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result shape of an integration's check-connection server action
|
|
3
|
+
* (the standard enhanceAction envelope).
|
|
4
|
+
*/
|
|
5
|
+
export interface ConnectionCheckResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
data?: {
|
|
8
|
+
obtainedAt?: string | null;
|
|
9
|
+
} | null;
|
|
10
|
+
error?: string;
|
|
11
|
+
}
|
|
12
|
+
export type ConnectionCheckAction = (input: {
|
|
13
|
+
accountId: string;
|
|
14
|
+
}) => Promise<ConnectionCheckResult>;
|
|
15
|
+
/**
|
|
16
|
+
* Optional OAuth failure query param an integration's connect route may
|
|
17
|
+
* append on redirect (e.g. dropbox_error=credentials_not_configured).
|
|
18
|
+
*/
|
|
19
|
+
export interface OAuthErrorParam {
|
|
20
|
+
name: string;
|
|
21
|
+
/** Map the raw param value to a user-facing message; return null to ignore. */
|
|
22
|
+
toMessage: (value: string) => string | null;
|
|
23
|
+
}
|
|
24
|
+
export interface UseOAuthConnectionStatusOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Cache key segment, by convention `<slug>-connection`. The full query key
|
|
27
|
+
* is ['integration-data', accountId, connectionKey] so all widgets and the
|
|
28
|
+
* activation form share one deduped request per dashboard load.
|
|
29
|
+
*/
|
|
30
|
+
connectionKey: string;
|
|
31
|
+
checkConnection: ConnectionCheckAction;
|
|
32
|
+
/** Gate the query (e.g. on integration-active state). Defaults to true. */
|
|
33
|
+
enabled?: boolean;
|
|
34
|
+
errorParam?: OAuthErrorParam;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Shared OAuth connection-status hook. Detects the `oauth_success=true`
|
|
38
|
+
* return from an integration's connect route, strips the transient params
|
|
39
|
+
* from the URL, and invalidates the cached status so dependent UI flips to
|
|
40
|
+
* connected without a manual refresh.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useOAuthConnectionStatus(accountId: string, options: UseOAuthConnectionStatusOptions): {
|
|
43
|
+
oauthJustCompleted: boolean;
|
|
44
|
+
oauthError: string | null;
|
|
45
|
+
data: NoInfer<{
|
|
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
|
+
dataUpdatedAt: number;
|
|
64
|
+
errorUpdatedAt: number;
|
|
65
|
+
failureCount: number;
|
|
66
|
+
failureReason: Error | null;
|
|
67
|
+
errorUpdateCount: number;
|
|
68
|
+
isFetched: boolean;
|
|
69
|
+
isFetchedAfterMount: boolean;
|
|
70
|
+
isFetching: boolean;
|
|
71
|
+
isInitialLoading: boolean;
|
|
72
|
+
isPaused: boolean;
|
|
73
|
+
isRefetching: boolean;
|
|
74
|
+
isStale: boolean;
|
|
75
|
+
isEnabled: boolean;
|
|
76
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
77
|
+
isConnected: boolean;
|
|
78
|
+
error: string | undefined;
|
|
79
|
+
obtainedAt?: undefined;
|
|
80
|
+
} | {
|
|
81
|
+
isConnected: boolean;
|
|
82
|
+
obtainedAt: string | null | undefined;
|
|
83
|
+
error?: undefined;
|
|
84
|
+
}>, Error>>;
|
|
85
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
86
|
+
promise: Promise<NoInfer<{
|
|
87
|
+
isConnected: boolean;
|
|
88
|
+
error: string | undefined;
|
|
89
|
+
obtainedAt?: undefined;
|
|
90
|
+
} | {
|
|
91
|
+
isConnected: boolean;
|
|
92
|
+
obtainedAt: string | null | undefined;
|
|
93
|
+
error?: undefined;
|
|
94
|
+
}>>;
|
|
95
|
+
} | {
|
|
96
|
+
oauthJustCompleted: boolean;
|
|
97
|
+
oauthError: string | null;
|
|
98
|
+
data: NoInfer<{
|
|
99
|
+
isConnected: boolean;
|
|
100
|
+
error: string | undefined;
|
|
101
|
+
obtainedAt?: undefined;
|
|
102
|
+
} | {
|
|
103
|
+
isConnected: boolean;
|
|
104
|
+
obtainedAt: string | null | undefined;
|
|
105
|
+
error?: undefined;
|
|
106
|
+
}>;
|
|
107
|
+
error: null;
|
|
108
|
+
isError: false;
|
|
109
|
+
isPending: false;
|
|
110
|
+
isLoading: false;
|
|
111
|
+
isLoadingError: false;
|
|
112
|
+
isRefetchError: false;
|
|
113
|
+
isSuccess: true;
|
|
114
|
+
isPlaceholderData: false;
|
|
115
|
+
status: "success";
|
|
116
|
+
dataUpdatedAt: number;
|
|
117
|
+
errorUpdatedAt: number;
|
|
118
|
+
failureCount: number;
|
|
119
|
+
failureReason: Error | null;
|
|
120
|
+
errorUpdateCount: number;
|
|
121
|
+
isFetched: boolean;
|
|
122
|
+
isFetchedAfterMount: boolean;
|
|
123
|
+
isFetching: boolean;
|
|
124
|
+
isInitialLoading: boolean;
|
|
125
|
+
isPaused: boolean;
|
|
126
|
+
isRefetching: boolean;
|
|
127
|
+
isStale: boolean;
|
|
128
|
+
isEnabled: boolean;
|
|
129
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
130
|
+
isConnected: boolean;
|
|
131
|
+
error: string | undefined;
|
|
132
|
+
obtainedAt?: undefined;
|
|
133
|
+
} | {
|
|
134
|
+
isConnected: boolean;
|
|
135
|
+
obtainedAt: string | null | undefined;
|
|
136
|
+
error?: undefined;
|
|
137
|
+
}>, Error>>;
|
|
138
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
139
|
+
promise: Promise<NoInfer<{
|
|
140
|
+
isConnected: boolean;
|
|
141
|
+
error: string | undefined;
|
|
142
|
+
obtainedAt?: undefined;
|
|
143
|
+
} | {
|
|
144
|
+
isConnected: boolean;
|
|
145
|
+
obtainedAt: string | null | undefined;
|
|
146
|
+
error?: undefined;
|
|
147
|
+
}>>;
|
|
148
|
+
} | {
|
|
149
|
+
oauthJustCompleted: boolean;
|
|
150
|
+
oauthError: string | null;
|
|
151
|
+
data: undefined;
|
|
152
|
+
error: Error;
|
|
153
|
+
isError: true;
|
|
154
|
+
isPending: false;
|
|
155
|
+
isLoading: false;
|
|
156
|
+
isLoadingError: true;
|
|
157
|
+
isRefetchError: false;
|
|
158
|
+
isSuccess: false;
|
|
159
|
+
isPlaceholderData: false;
|
|
160
|
+
status: "error";
|
|
161
|
+
dataUpdatedAt: number;
|
|
162
|
+
errorUpdatedAt: number;
|
|
163
|
+
failureCount: number;
|
|
164
|
+
failureReason: Error | null;
|
|
165
|
+
errorUpdateCount: number;
|
|
166
|
+
isFetched: boolean;
|
|
167
|
+
isFetchedAfterMount: boolean;
|
|
168
|
+
isFetching: boolean;
|
|
169
|
+
isInitialLoading: boolean;
|
|
170
|
+
isPaused: boolean;
|
|
171
|
+
isRefetching: boolean;
|
|
172
|
+
isStale: boolean;
|
|
173
|
+
isEnabled: boolean;
|
|
174
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
175
|
+
isConnected: boolean;
|
|
176
|
+
error: string | undefined;
|
|
177
|
+
obtainedAt?: undefined;
|
|
178
|
+
} | {
|
|
179
|
+
isConnected: boolean;
|
|
180
|
+
obtainedAt: string | null | undefined;
|
|
181
|
+
error?: undefined;
|
|
182
|
+
}>, Error>>;
|
|
183
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
184
|
+
promise: Promise<NoInfer<{
|
|
185
|
+
isConnected: boolean;
|
|
186
|
+
error: string | undefined;
|
|
187
|
+
obtainedAt?: undefined;
|
|
188
|
+
} | {
|
|
189
|
+
isConnected: boolean;
|
|
190
|
+
obtainedAt: string | null | undefined;
|
|
191
|
+
error?: undefined;
|
|
192
|
+
}>>;
|
|
193
|
+
} | {
|
|
194
|
+
oauthJustCompleted: boolean;
|
|
195
|
+
oauthError: string | null;
|
|
196
|
+
data: undefined;
|
|
197
|
+
error: null;
|
|
198
|
+
isError: false;
|
|
199
|
+
isPending: true;
|
|
200
|
+
isLoading: true;
|
|
201
|
+
isLoadingError: false;
|
|
202
|
+
isRefetchError: false;
|
|
203
|
+
isSuccess: false;
|
|
204
|
+
isPlaceholderData: false;
|
|
205
|
+
status: "pending";
|
|
206
|
+
dataUpdatedAt: number;
|
|
207
|
+
errorUpdatedAt: number;
|
|
208
|
+
failureCount: number;
|
|
209
|
+
failureReason: Error | null;
|
|
210
|
+
errorUpdateCount: number;
|
|
211
|
+
isFetched: boolean;
|
|
212
|
+
isFetchedAfterMount: boolean;
|
|
213
|
+
isFetching: boolean;
|
|
214
|
+
isInitialLoading: boolean;
|
|
215
|
+
isPaused: boolean;
|
|
216
|
+
isRefetching: boolean;
|
|
217
|
+
isStale: boolean;
|
|
218
|
+
isEnabled: boolean;
|
|
219
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
220
|
+
isConnected: boolean;
|
|
221
|
+
error: string | undefined;
|
|
222
|
+
obtainedAt?: undefined;
|
|
223
|
+
} | {
|
|
224
|
+
isConnected: boolean;
|
|
225
|
+
obtainedAt: string | null | undefined;
|
|
226
|
+
error?: undefined;
|
|
227
|
+
}>, Error>>;
|
|
228
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
229
|
+
promise: Promise<NoInfer<{
|
|
230
|
+
isConnected: boolean;
|
|
231
|
+
error: string | undefined;
|
|
232
|
+
obtainedAt?: undefined;
|
|
233
|
+
} | {
|
|
234
|
+
isConnected: boolean;
|
|
235
|
+
obtainedAt: string | null | undefined;
|
|
236
|
+
error?: undefined;
|
|
237
|
+
}>>;
|
|
238
|
+
} | {
|
|
239
|
+
oauthJustCompleted: boolean;
|
|
240
|
+
oauthError: string | null;
|
|
241
|
+
data: undefined;
|
|
242
|
+
error: null;
|
|
243
|
+
isError: false;
|
|
244
|
+
isPending: true;
|
|
245
|
+
isLoadingError: false;
|
|
246
|
+
isRefetchError: false;
|
|
247
|
+
isSuccess: false;
|
|
248
|
+
isPlaceholderData: false;
|
|
249
|
+
status: "pending";
|
|
250
|
+
dataUpdatedAt: number;
|
|
251
|
+
errorUpdatedAt: number;
|
|
252
|
+
failureCount: number;
|
|
253
|
+
failureReason: Error | null;
|
|
254
|
+
errorUpdateCount: number;
|
|
255
|
+
isFetched: boolean;
|
|
256
|
+
isFetchedAfterMount: boolean;
|
|
257
|
+
isFetching: boolean;
|
|
258
|
+
isLoading: boolean;
|
|
259
|
+
isInitialLoading: boolean;
|
|
260
|
+
isPaused: boolean;
|
|
261
|
+
isRefetching: boolean;
|
|
262
|
+
isStale: boolean;
|
|
263
|
+
isEnabled: boolean;
|
|
264
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
265
|
+
isConnected: boolean;
|
|
266
|
+
error: string | undefined;
|
|
267
|
+
obtainedAt?: undefined;
|
|
268
|
+
} | {
|
|
269
|
+
isConnected: boolean;
|
|
270
|
+
obtainedAt: string | null | undefined;
|
|
271
|
+
error?: undefined;
|
|
272
|
+
}>, Error>>;
|
|
273
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
274
|
+
promise: Promise<NoInfer<{
|
|
275
|
+
isConnected: boolean;
|
|
276
|
+
error: string | undefined;
|
|
277
|
+
obtainedAt?: undefined;
|
|
278
|
+
} | {
|
|
279
|
+
isConnected: boolean;
|
|
280
|
+
obtainedAt: string | null | undefined;
|
|
281
|
+
error?: undefined;
|
|
282
|
+
}>>;
|
|
283
|
+
} | {
|
|
284
|
+
oauthJustCompleted: boolean;
|
|
285
|
+
oauthError: string | null;
|
|
286
|
+
data: NoInfer<{
|
|
287
|
+
isConnected: boolean;
|
|
288
|
+
error: string | undefined;
|
|
289
|
+
obtainedAt?: undefined;
|
|
290
|
+
} | {
|
|
291
|
+
isConnected: boolean;
|
|
292
|
+
obtainedAt: string | null | undefined;
|
|
293
|
+
error?: undefined;
|
|
294
|
+
}>;
|
|
295
|
+
isError: false;
|
|
296
|
+
error: null;
|
|
297
|
+
isPending: false;
|
|
298
|
+
isLoading: false;
|
|
299
|
+
isLoadingError: false;
|
|
300
|
+
isRefetchError: false;
|
|
301
|
+
isSuccess: true;
|
|
302
|
+
isPlaceholderData: true;
|
|
303
|
+
status: "success";
|
|
304
|
+
dataUpdatedAt: number;
|
|
305
|
+
errorUpdatedAt: number;
|
|
306
|
+
failureCount: number;
|
|
307
|
+
failureReason: Error | null;
|
|
308
|
+
errorUpdateCount: number;
|
|
309
|
+
isFetched: boolean;
|
|
310
|
+
isFetchedAfterMount: boolean;
|
|
311
|
+
isFetching: boolean;
|
|
312
|
+
isInitialLoading: boolean;
|
|
313
|
+
isPaused: boolean;
|
|
314
|
+
isRefetching: boolean;
|
|
315
|
+
isStale: boolean;
|
|
316
|
+
isEnabled: boolean;
|
|
317
|
+
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<NoInfer<{
|
|
318
|
+
isConnected: boolean;
|
|
319
|
+
error: string | undefined;
|
|
320
|
+
obtainedAt?: undefined;
|
|
321
|
+
} | {
|
|
322
|
+
isConnected: boolean;
|
|
323
|
+
obtainedAt: string | null | undefined;
|
|
324
|
+
error?: undefined;
|
|
325
|
+
}>, Error>>;
|
|
326
|
+
fetchStatus: import("@tanstack/react-query").FetchStatus;
|
|
327
|
+
promise: Promise<NoInfer<{
|
|
328
|
+
isConnected: boolean;
|
|
329
|
+
error: string | undefined;
|
|
330
|
+
obtainedAt?: undefined;
|
|
331
|
+
} | {
|
|
332
|
+
isConnected: boolean;
|
|
333
|
+
obtainedAt: string | null | undefined;
|
|
334
|
+
error?: undefined;
|
|
335
|
+
}>>;
|
|
336
|
+
};
|
|
337
|
+
/**
|
|
338
|
+
* Resolves the gating booleans every OAuth-backed dashboard widget needs:
|
|
339
|
+
*
|
|
340
|
+
* - `enabled` — the integration is active and an account id is present.
|
|
341
|
+
* - `isConnected` — a live OAuth connection exists (access token).
|
|
342
|
+
* - `dataEnabled` — safe to fire the widget's data query (active + connected).
|
|
343
|
+
* - `connectionPending` — still resolving the connection status.
|
|
344
|
+
* - `connectionError` — the connection check itself failed (network/5xx).
|
|
345
|
+
* Distinct from `notConnected`: a transient failure should surface as an
|
|
346
|
+
* error, not as a misleading "connect X" prompt.
|
|
347
|
+
* - `notConnected` — activated, resolved without error, but no OAuth
|
|
348
|
+
* connection. The widget should render its "connect X" inactive state.
|
|
349
|
+
*
|
|
350
|
+
* The underlying connection check shares one query key across all widgets,
|
|
351
|
+
* so react-query dedupes it to a single request per dashboard load.
|
|
352
|
+
*/
|
|
353
|
+
export declare function useOAuthWidgetGate(accountId: string | undefined, integrationActive: boolean, options: Pick<UseOAuthConnectionStatusOptions, 'connectionKey' | 'checkConnection'>): {
|
|
354
|
+
enabled: boolean;
|
|
355
|
+
isConnected: boolean;
|
|
356
|
+
connectionPending: boolean;
|
|
357
|
+
connectionError: boolean;
|
|
358
|
+
dataEnabled: boolean;
|
|
359
|
+
notConnected: boolean;
|
|
360
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
/**
|
|
5
|
+
* Shared OAuth connection-status hook. Detects the `oauth_success=true`
|
|
6
|
+
* return from an integration's connect route, strips the transient params
|
|
7
|
+
* from the URL, and invalidates the cached status so dependent UI flips to
|
|
8
|
+
* connected without a manual refresh.
|
|
9
|
+
*/
|
|
10
|
+
export function useOAuthConnectionStatus(accountId, options) {
|
|
11
|
+
const { connectionKey, checkConnection, enabled = true, errorParam, } = options;
|
|
12
|
+
const queryClient = useQueryClient();
|
|
13
|
+
// Read the OAuth return params synchronously on mount (not in an effect)
|
|
14
|
+
// so the first render already knows whether we just came back from OAuth.
|
|
15
|
+
const [oauthJustCompleted] = useState(() => {
|
|
16
|
+
if (typeof window === 'undefined')
|
|
17
|
+
return false;
|
|
18
|
+
const params = new URLSearchParams(window.location.search);
|
|
19
|
+
return params.get('oauth_success') === 'true';
|
|
20
|
+
});
|
|
21
|
+
const [oauthError] = useState(() => {
|
|
22
|
+
if (!errorParam || typeof window === 'undefined')
|
|
23
|
+
return null;
|
|
24
|
+
const value = new URLSearchParams(window.location.search).get(errorParam.name);
|
|
25
|
+
return value ? errorParam.toMessage(value) : null;
|
|
26
|
+
});
|
|
27
|
+
const query = useQuery({
|
|
28
|
+
queryKey: ['integration-data', accountId, connectionKey],
|
|
29
|
+
queryFn: async () => {
|
|
30
|
+
var _a;
|
|
31
|
+
const result = await checkConnection({ accountId });
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
return { isConnected: false, error: result.error };
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
isConnected: true,
|
|
37
|
+
obtainedAt: (_a = result.data) === null || _a === void 0 ? void 0 : _a.obtainedAt,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
enabled: enabled && !!accountId,
|
|
41
|
+
staleTime: 5 * 60 * 1000,
|
|
42
|
+
refetchOnWindowFocus: false,
|
|
43
|
+
});
|
|
44
|
+
const errorParamName = errorParam === null || errorParam === void 0 ? void 0 : errorParam.name;
|
|
45
|
+
// Justified useEffect (per CLAUDE.md): after the OAuth redirect lands back
|
|
46
|
+
// on this page with `?oauth_success=true`, we must (1) strip the transient
|
|
47
|
+
// query params from the URL and (2) invalidate the cached connection status
|
|
48
|
+
// so widgets flip to connected without a manual refresh. Both are
|
|
49
|
+
// post-render side effects on external systems (history API + query
|
|
50
|
+
// cache), not derivable during render.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!oauthJustCompleted)
|
|
53
|
+
return;
|
|
54
|
+
const url = new URL(window.location.href);
|
|
55
|
+
url.searchParams.delete('oauth_success');
|
|
56
|
+
if (errorParamName) {
|
|
57
|
+
url.searchParams.delete(errorParamName);
|
|
58
|
+
}
|
|
59
|
+
window.history.replaceState({}, '', url.pathname + url.search);
|
|
60
|
+
queryClient.invalidateQueries({
|
|
61
|
+
queryKey: ['integration-data', accountId, connectionKey],
|
|
62
|
+
});
|
|
63
|
+
}, [
|
|
64
|
+
oauthJustCompleted,
|
|
65
|
+
accountId,
|
|
66
|
+
connectionKey,
|
|
67
|
+
errorParamName,
|
|
68
|
+
queryClient,
|
|
69
|
+
]);
|
|
70
|
+
return Object.assign(Object.assign({}, query), { oauthJustCompleted, oauthError });
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the gating booleans every OAuth-backed dashboard widget needs:
|
|
74
|
+
*
|
|
75
|
+
* - `enabled` — the integration is active and an account id is present.
|
|
76
|
+
* - `isConnected` — a live OAuth connection exists (access token).
|
|
77
|
+
* - `dataEnabled` — safe to fire the widget's data query (active + connected).
|
|
78
|
+
* - `connectionPending` — still resolving the connection status.
|
|
79
|
+
* - `connectionError` — the connection check itself failed (network/5xx).
|
|
80
|
+
* Distinct from `notConnected`: a transient failure should surface as an
|
|
81
|
+
* error, not as a misleading "connect X" prompt.
|
|
82
|
+
* - `notConnected` — activated, resolved without error, but no OAuth
|
|
83
|
+
* connection. The widget should render its "connect X" inactive state.
|
|
84
|
+
*
|
|
85
|
+
* The underlying connection check shares one query key across all widgets,
|
|
86
|
+
* so react-query dedupes it to a single request per dashboard load.
|
|
87
|
+
*/
|
|
88
|
+
export function useOAuthWidgetGate(accountId, integrationActive, options) {
|
|
89
|
+
var _a;
|
|
90
|
+
const enabled = integrationActive && !!accountId;
|
|
91
|
+
const query = useOAuthConnectionStatus(accountId !== null && accountId !== void 0 ? accountId : '', Object.assign(Object.assign({}, options), { enabled }));
|
|
92
|
+
const connectionPending = enabled && query.isPending;
|
|
93
|
+
const connectionError = enabled && query.isError;
|
|
94
|
+
const isConnected = ((_a = query.data) === null || _a === void 0 ? void 0 : _a.isConnected) === true;
|
|
95
|
+
const dataEnabled = enabled && isConnected;
|
|
96
|
+
// Only treat as "not connected" when the check resolved successfully with
|
|
97
|
+
// no token — a query error is an error state, not a disconnected account.
|
|
98
|
+
const notConnected = enabled && !connectionPending && !connectionError && !isConnected;
|
|
99
|
+
return {
|
|
100
|
+
enabled,
|
|
101
|
+
isConnected,
|
|
102
|
+
connectionPending,
|
|
103
|
+
connectionError,
|
|
104
|
+
dataEnabled,
|
|
105
|
+
notConnected,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=use-oauth-connection-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 enhanceAction 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 // Justified useEffect (per CLAUDE.md): after the OAuth redirect lands back\n // on this page with `?oauth_success=true`, we must (1) strip the transient\n // query params from the URL and (2) invalidate the cached connection status\n // so widgets flip to connected without a manual refresh. Both are\n // post-render side effects on external systems (history API + query\n // cache), not derivable during render.\n useEffect(() => {\n if (!oauthJustCompleted) 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 queryClient.invalidateQueries({\n queryKey: ['integration-data', accountId, connectionKey],\n });\n }, [\n oauthJustCompleted,\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"]}
|