@withone/connect 0.11.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -10,35 +10,60 @@
10
10
  /** Theme of One's hosted connect page. */
11
11
  export type OneConnectTheme = "light" | "dark";
12
12
 
13
- export interface OneConnectOptions {
13
+ /** Theme of the button: fixed, or following the visitor's setting. */
14
+ export type ConnectButtonTheme = "light" | "dark" | "auto";
15
+
16
+ /**
17
+ * Why a flow ended without a grant. The callback route puts only this
18
+ * code on the return URL; the text shown for it is fixed in the SDK, so
19
+ * a crafted link can never put its own words in front of the user.
20
+ *
21
+ * - `declined`: the user cancelled on One's page.
22
+ * - `expired`: the attempt took too long or was started elsewhere.
23
+ * - `failed`: One could not complete the connection.
24
+ */
25
+ export type ConnectFailureCode = "declined" | "expired" | "failed";
26
+
27
+ export interface OneConnectFlowOptions {
14
28
  /** The app's own backend authorize route. Relative paths such as
15
29
  * "/api/one/authorize" resolve against the page's origin. */
16
30
  authorizeUrl: string;
17
31
  /** Theme for One's hosted page. Carried on the URL fragment, which
18
32
  * survives the redirect chain, so the backend forwards nothing. */
33
+ connectTheme?: OneConnectTheme;
34
+ /** @deprecated Renamed to `connectTheme`; removed in the next minor. */
19
35
  appTheme?: OneConnectTheme;
20
- /** The grant completed and the backend stored the tokens. */
36
+ /** The grant completed and the backend stored the tokens. Fires once
37
+ * per page load, on the first flow still mounted when the tab
38
+ * returns. Treat it as a hint to refetch: your server is the truth. */
21
39
  onSuccess?: () => void;
22
- /** The flow ended without a grant: the user declined, the attempt
23
- * expired, or the exchange failed. `message` is safe to show. */
24
- onError?: (message: string) => void;
40
+ /** The flow ended without a grant. `message` is fixed text for
41
+ * `code`, safe to show. */
42
+ onError?: (message: string, code: ConnectFailureCode) => void;
43
+ /** The user came back with the browser's Back button before finishing
44
+ * (the page was restored from the back-forward cache). */
45
+ onCancel?: () => void;
25
46
  }
26
47
 
27
- export interface OneConnectHandle {
48
+ export interface OneConnectFlow {
28
49
  /** Navigates the tab to One's hosted connect flow. */
29
50
  open: () => void;
51
+ /** Swaps the options (callbacks, theme) without losing the flow. */
52
+ update: (options: OneConnectFlowOptions) => void;
53
+ /** Stops listening: callbacks no longer fire for this flow. */
54
+ destroy: () => void;
30
55
  }
31
56
 
32
- /** How the app's callback route reports the outcome on its final
33
- * redirect, read off the page URL when the tab returns. */
57
+ /** How the flow ended, read off the page URL when the tab returns. */
34
58
  export interface OneConnectReturn {
35
59
  status: "success" | "error";
60
+ code?: ConnectFailureCode;
36
61
  message?: string;
37
62
  }
38
63
 
39
64
  /**
40
65
  * A connector chip on the button. Pass One's connector slug ("stripe",
41
- * "google-calendar") and the SDK shows the logo and the name; pass an
66
+ * "google-calendar") and the SDK shows its logo and name; pass an
42
67
  * object to override either.
43
68
  */
44
69
  export type ConnectButtonPlatformInput =
@@ -52,33 +77,52 @@ export interface ConnectButtonPlatform {
52
77
  }
53
78
 
54
79
  export type ConnectButtonVariant = "default" | "accent" | "block";
80
+ export type ConnectButtonSize = "sm" | "md" | "lg";
55
81
  export type ConnectButtonState = "idle" | "connecting" | "connected";
56
82
 
57
- export interface ConnectButtonOptions {
58
- /** Everything the flow needs; the button wires open() and the
59
- * Connecting and Connected states around your callbacks. */
60
- connect: OneConnectOptions;
61
- /** "Connect your apps" unless overridden. */
62
- label?: string;
63
- /** default = neutral pill; accent = brand-colored pill; block =
64
- * full-width card with a description and a "Secured by One" foot. */
65
- variant?: ConnectButtonVariant;
66
- /** Matches the host page, not One's page (that is connect.appTheme). */
67
- theme?: OneConnectTheme;
68
- /** Connector chips. The first three render; the rest fold into a "+N"
69
- * chip, so that count only ever describes this list. */
83
+ /** One prop shape for every surface: React, Vue, Svelte, the custom
84
+ * element and `mountConnectButton`. */
85
+ export interface ConnectButtonProps {
86
+ /** The app's own backend authorize route; relative is fine. */
87
+ authorizeUrl: string;
88
+ /** Connector slugs, or objects that override the name or the logo.
89
+ * The first three draw as logos; the rest fold into a "+N" chip. */
70
90
  platforms?: ConnectButtonPlatformInput[];
71
- /** Sub-line on the block variant, shown while idle. */
72
- description?: string;
73
- /** Fill of the accent variant; One's lime when omitted. */
91
+ /** Whether this user has a live grant, from your server. When set, it
92
+ * decides the Connected state. When omitted, the button shows
93
+ * Connected only right after a successful return. */
94
+ connected?: boolean;
95
+ /** Not clickable, for example until terms are accepted. */
96
+ disabled?: boolean;
97
+ /** default = neutral, accent = your brand colour, block = a card with
98
+ * a description and a "Secured by One" foot. */
99
+ variant?: ConnectButtonVariant;
100
+ size?: ConnectButtonSize;
101
+ /** Stretches to the width of its container. */
102
+ fullWidth?: boolean;
103
+ /** Matches the host page. "auto" follows the visitor's setting. */
104
+ theme?: ConnectButtonTheme;
105
+ /** Theme of One's hosted page. */
106
+ connectTheme?: OneConnectTheme;
107
+ /** @deprecated Renamed to `connectTheme`; removed in the next minor. */
108
+ appTheme?: OneConnectTheme;
109
+ /** Fill of the accent variant; One's lime when omitted. The label is
110
+ * black or white, whichever reads better on it. */
74
111
  accentColor?: string;
75
- /** Label for the connected state. */
112
+ /** "Connect your apps" unless set. */
113
+ label?: string;
114
+ /** "Connected" unless set. */
76
115
  connectedLabel?: string;
116
+ /** Sub-line on the block variant. */
117
+ description?: string;
118
+ onSuccess?: () => void;
119
+ onError?: (message: string, code: ConnectFailureCode) => void;
120
+ onCancel?: () => void;
77
121
  }
78
122
 
79
123
  export interface ConnectButtonHandle {
80
- /** Override the visual state by hand. */
81
- setState: (state: ConnectButtonState) => void;
82
- /** Remove the button. */
124
+ /** Applies new props in place, keeping the button's state. */
125
+ update: (props: ConnectButtonProps) => void;
126
+ /** Removes the button and stops its callbacks. */
83
127
  destroy: () => void;
84
128
  }
package/src/vue.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * <ConnectButton
5
5
  * authorize-url="/api/one/authorize"
6
6
  * :platforms="['stripe', 'notion']"
7
+ * :connected="user.hasOneGrant"
7
8
  * @success="onConnected"
8
9
  * />
9
10
  *
@@ -20,65 +21,87 @@ import {
20
21
  import type { PropType } from "vue";
21
22
 
22
23
  import {
23
- mountConnectButton,
24
- optionsFromProps,
25
- propsIdentity,
24
+ renderConnectButton,
26
25
  type ConnectButtonHandle,
27
26
  type ConnectButtonPlatformInput,
27
+ type ConnectButtonProps,
28
+ type ConnectButtonSize,
29
+ type ConnectButtonTheme,
28
30
  type ConnectButtonVariant,
31
+ type ConnectFailureCode,
29
32
  type OneConnectTheme,
30
33
  } from "@withone/connect";
31
34
 
35
+ /** Booleans default to undefined, not false, so "not set" stays
36
+ * distinguishable from "false" (it matters for `connected`). */
37
+ const optionalBoolean = { type: Boolean, default: undefined };
38
+
32
39
  export const ConnectButton = defineComponent({
33
40
  name: "OneConnectButton",
34
41
  props: {
35
42
  authorizeUrl: { type: String, required: true },
36
- appTheme: { type: String as PropType<OneConnectTheme>, default: undefined },
37
- label: { type: String, default: undefined },
43
+ platforms: {
44
+ type: Array as PropType<ConnectButtonPlatformInput[]>,
45
+ default: undefined,
46
+ },
47
+ connected: optionalBoolean,
48
+ disabled: optionalBoolean,
38
49
  variant: {
39
50
  type: String as PropType<ConnectButtonVariant>,
40
51
  default: undefined,
41
52
  },
42
- theme: { type: String as PropType<OneConnectTheme>, default: undefined },
43
- platforms: {
44
- type: Array as PropType<ConnectButtonPlatformInput[]>,
53
+ size: { type: String as PropType<ConnectButtonSize>, default: undefined },
54
+ fullWidth: optionalBoolean,
55
+ theme: { type: String as PropType<ConnectButtonTheme>, default: undefined },
56
+ connectTheme: {
57
+ type: String as PropType<OneConnectTheme>,
45
58
  default: undefined,
46
59
  },
47
- description: { type: String, default: undefined },
60
+ /** @deprecated use connectTheme */
61
+ appTheme: { type: String as PropType<OneConnectTheme>, default: undefined },
48
62
  accentColor: { type: String, default: undefined },
63
+ label: { type: String, default: undefined },
49
64
  connectedLabel: { type: String, default: undefined },
65
+ description: { type: String, default: undefined },
50
66
  },
51
67
  emits: {
52
68
  success: () => true,
53
- error: (message: string) => typeof message === "string",
69
+ error: (message: string, code: ConnectFailureCode) =>
70
+ typeof message === "string" && typeof code === "string",
71
+ cancel: () => true,
54
72
  },
55
73
  setup(props, { emit }) {
56
- const container = ref<HTMLElement | null>(null);
74
+ const host = ref<HTMLElement | null>(null);
57
75
  let handle: ConnectButtonHandle | null = null;
58
76
 
59
- const mount = () => {
60
- handle?.destroy();
61
- handle = null;
62
- if (!container.value) return;
63
- handle = mountConnectButton(
64
- container.value,
65
- optionsFromProps(
66
- { ...props },
67
- {
68
- onSuccess: () => emit("success"),
69
- onError: (message) => emit("error", message),
70
- },
71
- ),
72
- );
73
- };
77
+ const current = (): ConnectButtonProps => ({
78
+ ...props,
79
+ onSuccess: () => emit("success"),
80
+ onError: (message, code) => emit("error", message, code),
81
+ onCancel: () => emit("cancel"),
82
+ });
74
83
 
75
- onMounted(mount);
76
- watch(() => propsIdentity({ ...props }), mount);
84
+ onMounted(() => {
85
+ if (host.value) handle = renderConnectButton(host.value, current());
86
+ });
87
+ watch(
88
+ () => ({ ...props }),
89
+ () => handle?.update(current()),
90
+ { deep: true },
91
+ );
77
92
  onBeforeUnmount(() => {
78
93
  handle?.destroy();
79
94
  handle = null;
80
95
  });
81
96
 
82
- return () => h("div", { ref: container });
97
+ // The host carries its layout attributes from the render, so server
98
+ // and client HTML agree; the button lives in its shadow root.
99
+ return () =>
100
+ h("span", {
101
+ ref: host,
102
+ class: "one-connect",
103
+ "data-variant": props.variant ?? "default",
104
+ "data-full-width": props.fullWidth ? "" : undefined,
105
+ });
83
106
  },
84
107
  });
@@ -1,13 +0,0 @@
1
- import type { OneConnectHandle, OneConnectOptions } from "./types";
2
- /**
3
- * Wires the connect flow to any element. A plain function rather than a
4
- * React hook, so it works from every framework: call it once where the
5
- * button lives and pass `open` to a click.
6
- *
7
- * The flow is a full-page redirect: the tab goes to the app's authorize
8
- * route, on to One's hosted page, back to the app's callback route, and
9
- * home. On the way home the callback route appends `?one_connect=…` to
10
- * the URL it redirects to; this function reads that on load, fires
11
- * onSuccess or onError once, and removes the params from the address bar.
12
- */
13
- export declare const useOneConnect: (options: OneConnectOptions) => OneConnectHandle;
@@ -1,28 +0,0 @@
1
- import type { ConnectButtonOptions, ConnectButtonPlatformInput, ConnectButtonVariant, OneConnectTheme } from "./types";
2
- /** The flat prop shape every framework wrapper exposes: React props,
3
- * Vue props, the Svelte action's options. One place turns it into the
4
- * core mountConnectButton options. */
5
- export interface ConnectButtonProps {
6
- /** The app's own backend authorize route; relative is fine. */
7
- authorizeUrl: string;
8
- /** Theme of One's hosted page. */
9
- appTheme?: OneConnectTheme;
10
- onSuccess?: () => void;
11
- onError?: (message: string) => void;
12
- label?: string;
13
- variant?: ConnectButtonVariant;
14
- /** Matches the host page. */
15
- theme?: OneConnectTheme;
16
- /** Connector slugs, or objects to override name or logo. */
17
- platforms?: ConnectButtonPlatformInput[];
18
- description?: string;
19
- accentColor?: string;
20
- connectedLabel?: string;
21
- }
22
- export interface ConnectButtonCallbacks {
23
- onSuccess?: () => void;
24
- onError?: (message: string) => void;
25
- }
26
- export declare function optionsFromProps(props: ConnectButtonProps, callbacks?: ConnectButtonCallbacks): ConnectButtonOptions;
27
- /** The inputs whose change should remount the button. */
28
- export declare function propsIdentity(props: ConnectButtonProps): string;
@@ -1,72 +0,0 @@
1
- import { THEME_PARAM } from "./constants";
2
- import { hasReturnParams, parseReturn, stripReturnParams } from "./return";
3
- import type { OneConnectHandle, OneConnectOptions } from "./types";
4
-
5
- /** The return leg is a page load, so it is delivered exactly once, to
6
- * the first instance that reads it. */
7
- let returnConsumed = false;
8
-
9
- const DEFAULT_ERROR = "The connection was not completed.";
10
-
11
- /**
12
- * Wires the connect flow to any element. A plain function rather than a
13
- * React hook, so it works from every framework: call it once where the
14
- * button lives and pass `open` to a click.
15
- *
16
- * The flow is a full-page redirect: the tab goes to the app's authorize
17
- * route, on to One's hosted page, back to the app's callback route, and
18
- * home. On the way home the callback route appends `?one_connect=…` to
19
- * the URL it redirects to; this function reads that on load, fires
20
- * onSuccess or onError once, and removes the params from the address bar.
21
- */
22
- export const useOneConnect = (options: OneConnectOptions): OneConnectHandle => {
23
- const buildAuthorizeUrl = (): string => {
24
- try {
25
- const url = new URL(
26
- options.authorizeUrl,
27
- typeof window === "undefined" ? undefined : window.location.origin,
28
- );
29
- if (options.appTheme) url.hash = `${THEME_PARAM}=${options.appTheme}`;
30
- return url.toString();
31
- } catch {
32
- return options.authorizeUrl;
33
- }
34
- };
35
-
36
- const open = () => {
37
- if (typeof window === "undefined") return;
38
- window.location.assign(buildAuthorizeUrl());
39
- };
40
-
41
- if (typeof window !== "undefined" && !returnConsumed) {
42
- const outcome = parseReturn(window.location.search);
43
- if (outcome) {
44
- returnConsumed = true;
45
- // Frameworks that manage the history themselves (the Next.js App
46
- // Router) sync the address bar back to their own URL when hydration
47
- // completes, undoing a single replaceState. Scrub now and re-check a
48
- // few beats later.
49
- const scrub = () => {
50
- if (!hasReturnParams(window.location.search)) return;
51
- window.history.replaceState(
52
- null,
53
- "",
54
- stripReturnParams(window.location),
55
- );
56
- };
57
- scrub();
58
- for (const delay of [50, 500, 2000]) window.setTimeout(scrub, delay);
59
-
60
- window.setTimeout(() => {
61
- try {
62
- if (outcome.status === "success") options.onSuccess?.();
63
- else options.onError?.(outcome.message ?? DEFAULT_ERROR);
64
- } catch {
65
- /* the app's own callback threw; not ours to handle */
66
- }
67
- }, 0);
68
- }
69
- }
70
-
71
- return { open };
72
- };
@@ -1,68 +0,0 @@
1
- import type {
2
- ConnectButtonOptions,
3
- ConnectButtonPlatformInput,
4
- ConnectButtonVariant,
5
- OneConnectTheme,
6
- } from "./types";
7
-
8
- /** The flat prop shape every framework wrapper exposes: React props,
9
- * Vue props, the Svelte action's options. One place turns it into the
10
- * core mountConnectButton options. */
11
- export interface ConnectButtonProps {
12
- /** The app's own backend authorize route; relative is fine. */
13
- authorizeUrl: string;
14
- /** Theme of One's hosted page. */
15
- appTheme?: OneConnectTheme;
16
- onSuccess?: () => void;
17
- onError?: (message: string) => void;
18
- label?: string;
19
- variant?: ConnectButtonVariant;
20
- /** Matches the host page. */
21
- theme?: OneConnectTheme;
22
- /** Connector slugs, or objects to override name or logo. */
23
- platforms?: ConnectButtonPlatformInput[];
24
- description?: string;
25
- accentColor?: string;
26
- connectedLabel?: string;
27
- }
28
-
29
- export interface ConnectButtonCallbacks {
30
- onSuccess?: () => void;
31
- onError?: (message: string) => void;
32
- }
33
-
34
- export function optionsFromProps(
35
- props: ConnectButtonProps,
36
- callbacks: ConnectButtonCallbacks = props,
37
- ): ConnectButtonOptions {
38
- return {
39
- connect: {
40
- authorizeUrl: props.authorizeUrl,
41
- appTheme: props.appTheme,
42
- onSuccess: () => callbacks.onSuccess?.(),
43
- onError: (message) => callbacks.onError?.(message),
44
- },
45
- label: props.label,
46
- variant: props.variant,
47
- theme: props.theme,
48
- platforms: props.platforms,
49
- description: props.description,
50
- accentColor: props.accentColor,
51
- connectedLabel: props.connectedLabel,
52
- };
53
- }
54
-
55
- /** The inputs whose change should remount the button. */
56
- export function propsIdentity(props: ConnectButtonProps): string {
57
- return JSON.stringify([
58
- props.authorizeUrl,
59
- props.appTheme,
60
- props.label,
61
- props.variant,
62
- props.theme,
63
- props.platforms ?? [],
64
- props.description,
65
- props.accentColor,
66
- props.connectedLabel,
67
- ]);
68
- }