@withone/connect 0.8.2 → 0.9.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 +158 -370
- package/dist/button.d.ts +9 -10
- package/dist/constants.d.ts +8 -4
- package/dist/index.cjs.js +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.esm.js +1 -1
- package/dist/next.cjs.js +104 -0
- package/dist/next.d.ts +36 -0
- package/dist/next.esm.js +98 -0
- package/dist/node.cjs.js +69 -0
- package/dist/node.d.ts +33 -0
- package/dist/node.esm.js +67 -0
- package/dist/platforms.d.ts +21 -0
- package/dist/react.cjs.js +1 -1
- package/dist/react.d.ts +2 -19
- package/dist/react.esm.js +1 -1
- package/dist/return.d.ts +11 -0
- package/dist/server/index.cjs.js +349 -0
- package/dist/server/index.d.ts +32 -0
- package/dist/server/index.esm.js +344 -0
- package/dist/server/oauth.d.ts +24 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/svelte.cjs.js +1 -1
- package/dist/svelte.d.ts +14 -4
- package/dist/svelte.esm.js +1 -1
- package/dist/types.d.ts +80 -0
- package/dist/useOneConnect.d.ts +13 -2
- package/dist/vue.cjs.js +1 -1
- package/dist/vue.d.ts +20 -18
- package/dist/vue.esm.js +1 -1
- package/dist/wrapper-options.d.ts +20 -15
- package/package.json +37 -15
- package/skills/one-connect/SKILL.md +194 -0
- package/src/button.ts +106 -137
- package/src/constants.ts +10 -4
- package/src/index.ts +17 -2
- package/src/next.ts +133 -0
- package/src/node.ts +107 -0
- package/src/platforms.ts +89 -0
- package/src/react.ts +23 -66
- package/src/return.ts +30 -0
- package/src/server/index.ts +382 -0
- package/src/server/oauth.ts +98 -0
- package/src/server/types.ts +155 -0
- package/src/svelte.ts +15 -15
- package/src/types.ts +87 -0
- package/src/useOneConnect.ts +36 -69
- package/src/vue.ts +23 -29
- package/src/wrapper-options.ts +46 -23
- package/dist/index.umd.js +0 -1
- package/dist/types/index.d.ts +0 -92
- package/src/react-types.d.ts +0 -23
- package/src/svelte-types.d.ts +0 -27
- package/src/types/index.d.ts +0 -92
- package/src/vue-types.d.ts +0 -16
package/src/button.ts
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
|
+
import { normalizePlatforms, parsePlatformsAttribute } from "./platforms";
|
|
1
2
|
import { useOneConnect } from "./useOneConnect";
|
|
2
3
|
import type {
|
|
3
4
|
ConnectButtonHandle,
|
|
4
5
|
ConnectButtonOptions,
|
|
6
|
+
ConnectButtonState,
|
|
5
7
|
OneConnectHandle,
|
|
6
8
|
} from "./types";
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the same way. Zero dependencies, zero One URLs — provider icons are
|
|
14
|
-
* passed in by the consumer.
|
|
11
|
+
* The pre-built trigger for the connect flow. Optional: `useOneConnect`
|
|
12
|
+
* with any element remains fully supported. Framework-agnostic: it
|
|
13
|
+
* renders real DOM into a container, so React, Vue, Svelte and plain
|
|
14
|
+
* HTML all mount it the same way.
|
|
15
15
|
*
|
|
16
|
-
* The button owns the flow wiring: click opens the
|
|
17
|
-
* turns "Connecting
|
|
18
|
-
*
|
|
19
|
-
* verb never changes).
|
|
16
|
+
* The button owns the flow wiring: a click opens the flow, the label
|
|
17
|
+
* turns "Connecting" while the tab is away, and lands on "Connected"
|
|
18
|
+
* when the return carries a success.
|
|
20
19
|
*/
|
|
21
20
|
|
|
22
21
|
const STYLE_ID = "one-connect-button-styles";
|
|
23
22
|
const EASE = "cubic-bezier(.2,.9,.25,1)";
|
|
23
|
+
const MAX_VISIBLE_CHIPS = 3;
|
|
24
|
+
|
|
25
|
+
/** One's palette (see the Clockwork design system): lime is the brand
|
|
26
|
+
* call-to-action, spring is success, carbon is the ink. */
|
|
27
|
+
const LIME = "#CCFF00";
|
|
28
|
+
const SPRING = "#3FE3A5";
|
|
29
|
+
const CARBON = "#0A0C0B";
|
|
24
30
|
|
|
25
31
|
interface Palette {
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
buttonBackground: string;
|
|
33
|
+
buttonForeground: string;
|
|
28
34
|
surface: string;
|
|
29
|
-
|
|
35
|
+
foreground: string;
|
|
30
36
|
muted: string;
|
|
31
37
|
line: string;
|
|
32
38
|
shadow: string;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
const LIGHT: Palette = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
surface: "#
|
|
39
|
-
|
|
42
|
+
buttonBackground: CARBON,
|
|
43
|
+
buttonForeground: "#FFFFFF",
|
|
44
|
+
surface: "#FFFFFF",
|
|
45
|
+
foreground: CARBON,
|
|
40
46
|
muted: "#6B7280",
|
|
41
47
|
line: "#D1D5DB",
|
|
42
48
|
shadow: "0 1px 2px rgba(10,12,11,.08), 0 4px 14px rgba(10,12,11,.06)",
|
|
43
49
|
};
|
|
44
50
|
|
|
45
51
|
const DARK: Palette = {
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
buttonBackground: "#F2F5F3",
|
|
53
|
+
buttonForeground: CARBON,
|
|
48
54
|
surface: "#101312",
|
|
49
|
-
|
|
55
|
+
foreground: "#F2F5F3",
|
|
50
56
|
muted: "#8A938E",
|
|
51
57
|
line: "#2A302D",
|
|
52
58
|
shadow: "0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.35)",
|
|
53
59
|
};
|
|
54
60
|
|
|
55
|
-
const LIME = "#CCFF00";
|
|
56
|
-
const SPRING = "#3FE3A5";
|
|
57
|
-
const CARBON = "#0A0C0B";
|
|
58
|
-
|
|
59
|
-
/** The One ring mark — same glyph the card footer pairs with the
|
|
60
|
-
* wordmark. Inline so the SDK stays free of One URLs. */
|
|
61
61
|
const ONE_MARK_SVG =
|
|
62
62
|
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" style="flex-shrink:0"><circle cx="12" cy="12" r="8.5"/></svg>';
|
|
63
63
|
const ARROW_SVG =
|
|
@@ -77,16 +77,17 @@ function ensureStyles(): void {
|
|
|
77
77
|
`.owcb:focus-visible{outline:2px solid ${SPRING};outline-offset:3px}`,
|
|
78
78
|
".owcb[disabled]{cursor:default;opacity:.75;transform:none}",
|
|
79
79
|
".owcb-stack{display:inline-flex;align-items:center;flex-shrink:0}",
|
|
80
|
-
`.owcb-chip{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;overflow:hidden;margin-left:-7px;transition:margin-left .28s ${EASE};position:relative}`,
|
|
80
|
+
`.owcb-chip{width:22px;height:22px;border-radius:6px;display:grid;place-items:center;overflow:hidden;margin-left:-7px;transition:margin-left .28s ${EASE};position:relative;background:#FFFFFF}`,
|
|
81
81
|
".owcb-chip:first-child{margin-left:0}",
|
|
82
82
|
".owcb-chip img{width:14px;height:14px;display:block;object-fit:contain}",
|
|
83
|
-
|
|
83
|
+
`.owcb-chip.owcb-more{font-family:ui-monospace,monospace;font-size:9px;font-weight:500;letter-spacing:-.02em;color:${CARBON}}`,
|
|
84
|
+
`.owcb-chip.owcb-letter{font:600 10px ui-monospace,monospace;color:${CARBON}}`,
|
|
84
85
|
".owcb:hover .owcb-chip{margin-left:-2px}",
|
|
85
86
|
".owcb:hover .owcb-chip:first-child{margin-left:0}",
|
|
86
87
|
".owcb-label{white-space:nowrap}",
|
|
87
88
|
`.owcb-arrow{opacity:.5;transition:transform .2s ${EASE},opacity .2s ${EASE}}`,
|
|
88
89
|
".owcb:hover .owcb-arrow{transform:translateX(2px);opacity:.8}",
|
|
89
|
-
|
|
90
|
+
".owcb-spinner{width:16px;height:16px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;opacity:.7;animation:owcb-spin .7s linear infinite}",
|
|
90
91
|
"@keyframes owcb-spin{to{transform:rotate(360deg)}}",
|
|
91
92
|
".owcb.owcb-block{display:flex;flex-direction:column;align-items:stretch;width:100%;max-width:420px;text-align:left;padding:16px;border-radius:14px;gap:0}",
|
|
92
93
|
".owcb-block-top{display:flex;align-items:center;justify-content:space-between;gap:12px}",
|
|
@@ -101,12 +102,8 @@ function ensureStyles(): void {
|
|
|
101
102
|
function buildStack(
|
|
102
103
|
options: ConnectButtonOptions,
|
|
103
104
|
chipRing: string,
|
|
104
|
-
moreColor: string,
|
|
105
105
|
): HTMLElement | null {
|
|
106
|
-
|
|
107
|
-
// into a single "+N" chip so the button stays clean and uncrowded.
|
|
108
|
-
const MAX_VISIBLE_CHIPS = 3;
|
|
109
|
-
const platforms = options.platforms ?? [];
|
|
106
|
+
const platforms = normalizePlatforms(options.platforms);
|
|
110
107
|
if (platforms.length === 0 && !options.moreCount) return null;
|
|
111
108
|
const stack = document.createElement("span");
|
|
112
109
|
stack.className = "owcb-stack";
|
|
@@ -114,24 +111,17 @@ function buildStack(
|
|
|
114
111
|
for (const platform of platforms.slice(0, MAX_VISIBLE_CHIPS)) {
|
|
115
112
|
const chip = document.createElement("span");
|
|
116
113
|
chip.className = "owcb-chip";
|
|
117
|
-
chip.
|
|
114
|
+
chip.title = platform.name;
|
|
118
115
|
chip.style.boxShadow = `0 0 0 1.5px ${chipRing}`;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
img.
|
|
124
|
-
|
|
125
|
-
chip.textContent = platform.name.charAt(0).toUpperCase();
|
|
126
|
-
chip.style.font = "600 10px ui-monospace,monospace";
|
|
127
|
-
chip.style.color = CARBON;
|
|
128
|
-
});
|
|
129
|
-
chip.appendChild(img);
|
|
130
|
-
} else {
|
|
116
|
+
const img = document.createElement("img");
|
|
117
|
+
img.alt = "";
|
|
118
|
+
img.src = platform.imageUrl;
|
|
119
|
+
img.addEventListener("error", () => {
|
|
120
|
+
img.remove();
|
|
121
|
+
chip.classList.add("owcb-letter");
|
|
131
122
|
chip.textContent = platform.name.charAt(0).toUpperCase();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
123
|
+
});
|
|
124
|
+
chip.appendChild(img);
|
|
135
125
|
stack.appendChild(chip);
|
|
136
126
|
}
|
|
137
127
|
const extra =
|
|
@@ -140,9 +130,7 @@ function buildStack(
|
|
|
140
130
|
if (extra > 0) {
|
|
141
131
|
const more = document.createElement("span");
|
|
142
132
|
more.className = "owcb-chip owcb-more";
|
|
143
|
-
more.style.background = "#ffffff";
|
|
144
133
|
more.style.boxShadow = `0 0 0 1.5px ${chipRing}`;
|
|
145
|
-
more.style.color = moreColor;
|
|
146
134
|
more.textContent = `+${extra}`;
|
|
147
135
|
stack.appendChild(more);
|
|
148
136
|
}
|
|
@@ -158,23 +146,20 @@ export const mountConnectButton = (
|
|
|
158
146
|
const palette = options.theme === "dark" ? DARK : LIGHT;
|
|
159
147
|
const variant = options.variant ?? "default";
|
|
160
148
|
const label = options.label ?? "Connect your apps";
|
|
149
|
+
const connectedLabel = options.connectedLabel ?? "Connected";
|
|
161
150
|
|
|
162
|
-
let state:
|
|
151
|
+
let state: ConnectButtonState = "idle";
|
|
163
152
|
let destroyed = false;
|
|
164
153
|
|
|
165
|
-
const
|
|
154
|
+
const flow: OneConnectHandle = useOneConnect({
|
|
166
155
|
...options.connect,
|
|
167
156
|
onSuccess: () => {
|
|
168
157
|
setState("connected");
|
|
169
158
|
options.connect.onSuccess?.();
|
|
170
159
|
},
|
|
171
|
-
onError: (
|
|
160
|
+
onError: (message) => {
|
|
172
161
|
setState("idle");
|
|
173
|
-
options.connect.onError?.(
|
|
174
|
-
},
|
|
175
|
-
onClose: () => {
|
|
176
|
-
setState("idle");
|
|
177
|
-
options.connect.onClose?.();
|
|
162
|
+
options.connect.onError?.(message);
|
|
178
163
|
},
|
|
179
164
|
});
|
|
180
165
|
|
|
@@ -182,52 +167,54 @@ export const mountConnectButton = (
|
|
|
182
167
|
button.type = "button";
|
|
183
168
|
button.className = variant === "block" ? "owcb owcb-block" : "owcb";
|
|
184
169
|
button.addEventListener("click", () => {
|
|
185
|
-
if (state
|
|
170
|
+
if (state === "connecting") return;
|
|
186
171
|
setState("connecting");
|
|
187
|
-
|
|
172
|
+
flow.open();
|
|
188
173
|
});
|
|
189
174
|
|
|
175
|
+
const titleFor = (): string =>
|
|
176
|
+
state === "connecting"
|
|
177
|
+
? "Connecting…"
|
|
178
|
+
: state === "connected"
|
|
179
|
+
? connectedLabel
|
|
180
|
+
: label;
|
|
181
|
+
|
|
190
182
|
const paint = () => {
|
|
191
183
|
const accent = variant === "accent";
|
|
192
184
|
const block = variant === "block";
|
|
193
|
-
const
|
|
185
|
+
const background = block
|
|
194
186
|
? palette.surface
|
|
195
187
|
: accent
|
|
196
188
|
? (options.accentColor ?? LIME)
|
|
197
|
-
: palette.
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
button.style.color = state === "connected" && !block ? CARBON :
|
|
189
|
+
: palette.buttonBackground;
|
|
190
|
+
const foreground = block
|
|
191
|
+
? palette.foreground
|
|
192
|
+
: accent
|
|
193
|
+
? CARBON
|
|
194
|
+
: palette.buttonForeground;
|
|
195
|
+
const chipRing = block ? palette.surface : background;
|
|
196
|
+
|
|
197
|
+
button.style.background =
|
|
198
|
+
state === "connected" && !block ? SPRING : background;
|
|
199
|
+
button.style.color = state === "connected" && !block ? CARBON : foreground;
|
|
208
200
|
button.style.boxShadow = block
|
|
209
201
|
? `inset 0 0 0 1px ${palette.line}`
|
|
210
202
|
: state === "connected"
|
|
211
203
|
? "none"
|
|
212
204
|
: accent
|
|
213
|
-
? `0 1px 2px rgba(10,12,11,.1), 0 6px 20px ${
|
|
205
|
+
? `0 1px 2px rgba(10,12,11,.1), 0 6px 20px ${options.accentColor ?? LIME}38`
|
|
214
206
|
: palette.shadow;
|
|
215
207
|
button.disabled = state === "connecting";
|
|
216
208
|
button.innerHTML = "";
|
|
217
209
|
|
|
218
|
-
const stack = buildStack(options, chipRing
|
|
210
|
+
const stack = buildStack(options, chipRing);
|
|
219
211
|
|
|
220
212
|
if (block) {
|
|
221
213
|
const top = document.createElement("span");
|
|
222
214
|
top.className = "owcb-block-top";
|
|
223
215
|
const title = document.createElement("span");
|
|
224
216
|
title.className = "owcb-block-title";
|
|
225
|
-
title.textContent =
|
|
226
|
-
state === "connecting"
|
|
227
|
-
? "Connecting…"
|
|
228
|
-
: state === "connected"
|
|
229
|
-
? (options.connectedLabel ?? "Connected")
|
|
230
|
-
: label;
|
|
217
|
+
title.textContent = titleFor();
|
|
231
218
|
top.appendChild(title);
|
|
232
219
|
if (state === "connecting") {
|
|
233
220
|
const spinner = document.createElement("span");
|
|
@@ -244,17 +231,14 @@ export const mountConnectButton = (
|
|
|
244
231
|
sub.textContent = options.description;
|
|
245
232
|
button.appendChild(sub);
|
|
246
233
|
}
|
|
247
|
-
// Mirrors the card/overlay footer strip: "Secured by" + ring mark
|
|
248
|
-
// + "one" wordmark, so the button and the surface it opens read as
|
|
249
|
-
// the same product.
|
|
250
234
|
const foot = document.createElement("span");
|
|
251
235
|
foot.className = "owcb-block-foot";
|
|
252
236
|
foot.style.color = palette.muted;
|
|
253
237
|
foot.style.borderTop = `1px solid ${palette.line}`;
|
|
254
238
|
foot.innerHTML =
|
|
255
239
|
`<span style="font-size:11px;color:${palette.muted}">Secured by</span>` +
|
|
256
|
-
`<span style="display:inline-flex;color:${palette.
|
|
257
|
-
`<span style="font-size:12px;font-weight:600;letter-spacing:-0.02em;color:${palette.
|
|
240
|
+
`<span style="display:inline-flex;color:${palette.foreground}">${ONE_MARK_SVG}</span>` +
|
|
241
|
+
`<span style="font-size:12px;font-weight:600;letter-spacing:-0.02em;color:${palette.foreground}">one</span>`;
|
|
258
242
|
button.appendChild(foot);
|
|
259
243
|
return;
|
|
260
244
|
}
|
|
@@ -274,12 +258,7 @@ export const mountConnectButton = (
|
|
|
274
258
|
|
|
275
259
|
const text = document.createElement("span");
|
|
276
260
|
text.className = "owcb-label";
|
|
277
|
-
text.textContent =
|
|
278
|
-
state === "connecting"
|
|
279
|
-
? "Connecting…"
|
|
280
|
-
: state === "connected"
|
|
281
|
-
? (options.connectedLabel ?? "Connected")
|
|
282
|
-
: label;
|
|
261
|
+
text.textContent = titleFor();
|
|
283
262
|
button.appendChild(text);
|
|
284
263
|
|
|
285
264
|
if (state === "idle") {
|
|
@@ -290,7 +269,7 @@ export const mountConnectButton = (
|
|
|
290
269
|
}
|
|
291
270
|
};
|
|
292
271
|
|
|
293
|
-
const setState = (next:
|
|
272
|
+
const setState = (next: ConnectButtonState) => {
|
|
294
273
|
if (destroyed) return;
|
|
295
274
|
state = next;
|
|
296
275
|
paint();
|
|
@@ -303,26 +282,24 @@ export const mountConnectButton = (
|
|
|
303
282
|
setState,
|
|
304
283
|
destroy: () => {
|
|
305
284
|
destroyed = true;
|
|
306
|
-
handle.close({ keepResult: true });
|
|
307
285
|
button.remove();
|
|
308
286
|
},
|
|
309
287
|
};
|
|
310
288
|
};
|
|
311
289
|
|
|
312
290
|
/**
|
|
313
|
-
* <one-connect-button
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
* (Next.js SSR) never touches HTMLElement.
|
|
291
|
+
* <one-connect-button>: one tag for any framework. The element wires the
|
|
292
|
+
* whole flow from its attributes and manages Connect, Connecting and
|
|
293
|
+
* Connected. Registered automatically in the browser on import; defined
|
|
294
|
+
* inside a function so importing this module on a server never touches
|
|
295
|
+
* HTMLElement.
|
|
319
296
|
*
|
|
320
297
|
* Attributes: authorize-url (required), app-theme, label, variant,
|
|
321
|
-
* theme, platforms (
|
|
298
|
+
* theme, platforms ("stripe, notion" or a JSON array), more-count,
|
|
322
299
|
* description, accent-color, connected-label.
|
|
323
|
-
* Events: "success"
|
|
324
|
-
*
|
|
325
|
-
*
|
|
300
|
+
* Events: "success" and "error" (detail: message), plus the matching
|
|
301
|
+
* onSuccess and onError function properties that React 19, Vue and
|
|
302
|
+
* Svelte set naturally as props.
|
|
326
303
|
*/
|
|
327
304
|
export function registerConnectButton(): void {
|
|
328
305
|
if (typeof window === "undefined" || typeof customElements === "undefined")
|
|
@@ -344,8 +321,7 @@ export function registerConnectButton(): void {
|
|
|
344
321
|
];
|
|
345
322
|
|
|
346
323
|
onSuccess: (() => void) | null = null;
|
|
347
|
-
onError: ((
|
|
348
|
-
onClose: (() => void) | null = null;
|
|
324
|
+
onError: ((message: string) => void) | null = null;
|
|
349
325
|
|
|
350
326
|
private handle: ConnectButtonHandle | null = null;
|
|
351
327
|
|
|
@@ -366,48 +342,28 @@ export function registerConnectButton(): void {
|
|
|
366
342
|
this.handle?.destroy();
|
|
367
343
|
this.handle = null;
|
|
368
344
|
const authorizeUrl = this.getAttribute("authorize-url");
|
|
369
|
-
if (!authorizeUrl) return;
|
|
370
|
-
|
|
371
|
-
let platforms: ConnectButtonOptions["platforms"];
|
|
372
|
-
const rawPlatforms = this.getAttribute("platforms");
|
|
373
|
-
if (rawPlatforms) {
|
|
374
|
-
try {
|
|
375
|
-
const parsed = JSON.parse(rawPlatforms) as unknown;
|
|
376
|
-
if (Array.isArray(parsed)) platforms = parsed;
|
|
377
|
-
} catch {
|
|
378
|
-
/* malformed platforms JSON — render without chips */
|
|
379
|
-
}
|
|
380
|
-
}
|
|
345
|
+
if (!authorizeUrl) return;
|
|
381
346
|
|
|
382
347
|
const moreCountRaw = this.getAttribute("more-count");
|
|
383
|
-
const moreCount = moreCountRaw ? parseInt(moreCountRaw, 10) :
|
|
348
|
+
const moreCount = moreCountRaw ? parseInt(moreCountRaw, 10) : NaN;
|
|
384
349
|
|
|
385
350
|
this.handle = mountConnectButton(this, {
|
|
386
351
|
connect: {
|
|
387
|
-
|
|
388
|
-
appTheme:
|
|
389
|
-
(this.getAttribute("app-theme") as "light" | "dark" | null) ??
|
|
390
|
-
undefined,
|
|
352
|
+
authorizeUrl,
|
|
353
|
+
appTheme: attributeAs(this, "app-theme", ["light", "dark"]),
|
|
391
354
|
onSuccess: () => {
|
|
392
355
|
this.onSuccess?.();
|
|
393
356
|
this.dispatchEvent(new CustomEvent("success"));
|
|
394
357
|
},
|
|
395
|
-
onError: (
|
|
396
|
-
this.onError?.(
|
|
397
|
-
this.dispatchEvent(new CustomEvent("error", { detail:
|
|
398
|
-
},
|
|
399
|
-
onClose: () => {
|
|
400
|
-
this.onClose?.();
|
|
401
|
-
this.dispatchEvent(new CustomEvent("close"));
|
|
358
|
+
onError: (message) => {
|
|
359
|
+
this.onError?.(message);
|
|
360
|
+
this.dispatchEvent(new CustomEvent("error", { detail: message }));
|
|
402
361
|
},
|
|
403
362
|
},
|
|
404
363
|
label: this.getAttribute("label") ?? undefined,
|
|
405
|
-
variant:
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
theme:
|
|
409
|
-
(this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
|
|
410
|
-
platforms,
|
|
364
|
+
variant: attributeAs(this, "variant", ["default", "accent", "block"]),
|
|
365
|
+
theme: attributeAs(this, "theme", ["light", "dark"]),
|
|
366
|
+
platforms: parsePlatformsAttribute(this.getAttribute("platforms")),
|
|
411
367
|
moreCount: Number.isFinite(moreCount) ? moreCount : undefined,
|
|
412
368
|
description: this.getAttribute("description") ?? undefined,
|
|
413
369
|
accentColor: this.getAttribute("accent-color") ?? undefined,
|
|
@@ -419,4 +375,17 @@ export function registerConnectButton(): void {
|
|
|
419
375
|
customElements.define("one-connect-button", OneConnectButtonElement);
|
|
420
376
|
}
|
|
421
377
|
|
|
378
|
+
/** An attribute narrowed to a known set of values; anything else is
|
|
379
|
+
* treated as unset rather than passed through. */
|
|
380
|
+
function attributeAs<T extends string>(
|
|
381
|
+
element: HTMLElement,
|
|
382
|
+
name: string,
|
|
383
|
+
allowed: readonly T[],
|
|
384
|
+
): T | undefined {
|
|
385
|
+
const value = element.getAttribute(name);
|
|
386
|
+
return value && (allowed as readonly string[]).includes(value)
|
|
387
|
+
? (value as T)
|
|
388
|
+
: undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
422
391
|
registerConnectButton();
|
package/src/constants.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
/** Query params the
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/** Query params the app's callback route puts on its final redirect.
|
|
2
|
+
* The SDK reads them off the page URL when the tab comes home, so
|
|
3
|
+
* there is no completion page to build. */
|
|
4
4
|
export const RETURN_STATUS_PARAM = "one_connect";
|
|
5
5
|
export const RETURN_MESSAGE_PARAM = "one_connect_message";
|
|
6
6
|
|
|
7
7
|
/** Fragment key on the authorize URL carrying the app-chosen theme.
|
|
8
8
|
* A fragment never reaches any server and survives the whole redirect
|
|
9
|
-
* chain, so the
|
|
9
|
+
* chain, so the app's backend forwards nothing. */
|
|
10
10
|
export const THEME_PARAM = "one_theme";
|
|
11
|
+
|
|
12
|
+
/** One's connector logos, by slug. */
|
|
13
|
+
export const CONNECTOR_ASSETS_URL = "https://assets.withone.ai/connectors";
|
|
14
|
+
|
|
15
|
+
/** One's production API. Point `oneApiUrl` elsewhere for development. */
|
|
16
|
+
export const DEFAULT_ONE_API_URL = "https://api.withone.ai";
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
export { useOneConnect } from "./useOneConnect";
|
|
2
2
|
export { mountConnectButton, registerConnectButton } from "./button";
|
|
3
|
+
export {
|
|
4
|
+
optionsFromProps,
|
|
5
|
+
propsIdentity,
|
|
6
|
+
type ConnectButtonProps,
|
|
7
|
+
type ConnectButtonCallbacks,
|
|
8
|
+
} from "./wrapper-options";
|
|
9
|
+
export {
|
|
10
|
+
normalizePlatform,
|
|
11
|
+
normalizePlatforms,
|
|
12
|
+
parsePlatformsAttribute,
|
|
13
|
+
} from "./platforms";
|
|
3
14
|
export type {
|
|
4
|
-
|
|
15
|
+
OneConnectOptions,
|
|
5
16
|
OneConnectHandle,
|
|
6
|
-
|
|
17
|
+
OneConnectReturn,
|
|
18
|
+
OneConnectTheme,
|
|
7
19
|
ConnectButtonOptions,
|
|
8
20
|
ConnectButtonPlatform,
|
|
21
|
+
ConnectButtonPlatformInput,
|
|
22
|
+
ConnectButtonVariant,
|
|
23
|
+
ConnectButtonState,
|
|
9
24
|
ConnectButtonHandle,
|
|
10
25
|
} from "./types";
|
package/src/next.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@withone/connect/next`: the two routes as one file, for any server
|
|
3
|
+
* that speaks the web `Request` and `Response` (Next.js App Router,
|
|
4
|
+
* Remix, SvelteKit, Nuxt/h3 via its adapters, Hono, Bun).
|
|
5
|
+
*
|
|
6
|
+
* // app/api/one/[action]/route.ts
|
|
7
|
+
* import { createOneConnectRoutes } from "@withone/connect/next";
|
|
8
|
+
* import { oneConnect } from "@/lib/one";
|
|
9
|
+
*
|
|
10
|
+
* export const { GET } = createOneConnectRoutes(oneConnect, {
|
|
11
|
+
* identifyUser: async (request) => getSessionUserId(request),
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* That serves /api/one/authorize and /api/one/callback. Register
|
|
15
|
+
* `https://yourapp.com/api/one/callback` as the app's redirect URI.
|
|
16
|
+
*/
|
|
17
|
+
import type { OneConnect } from "./server";
|
|
18
|
+
|
|
19
|
+
export interface OneConnectRoutesOptions {
|
|
20
|
+
/** The app's own id for the signed-in user, or null when nobody is
|
|
21
|
+
* signed in. The grant is stored under this id. */
|
|
22
|
+
identifyUser: (request: Request) => Promise<string | null> | string | null;
|
|
23
|
+
/** Pre-fills the sign-in email on One's page for this user. */
|
|
24
|
+
loginHintFor?: (request: Request) => Promise<string | null> | string | null;
|
|
25
|
+
/** Where to send the browser when nobody is signed in. Answers 401
|
|
26
|
+
* when omitted. */
|
|
27
|
+
signInUrl?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OneConnectRoutes {
|
|
31
|
+
GET: (request: Request) => Promise<Response>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const AUTHORIZE_ROUTE = "authorize";
|
|
35
|
+
export const CALLBACK_ROUTE = "callback";
|
|
36
|
+
|
|
37
|
+
function redirectWithCookies(
|
|
38
|
+
location: string,
|
|
39
|
+
setCookies: string[],
|
|
40
|
+
): Response {
|
|
41
|
+
const headers = new Headers({ Location: location });
|
|
42
|
+
for (const cookie of setCookies) headers.append("Set-Cookie", cookie);
|
|
43
|
+
return new Response(null, { status: 302, headers });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function serializeCookie(
|
|
47
|
+
name: string,
|
|
48
|
+
value: string,
|
|
49
|
+
options: { httpOnly: boolean; secure: boolean; sameSite: "lax"; maxAge: number; path: string },
|
|
50
|
+
): string {
|
|
51
|
+
const parts = [
|
|
52
|
+
`${name}=${encodeURIComponent(value)}`,
|
|
53
|
+
`Path=${options.path}`,
|
|
54
|
+
`Max-Age=${options.maxAge}`,
|
|
55
|
+
`SameSite=Lax`,
|
|
56
|
+
];
|
|
57
|
+
if (options.httpOnly) parts.push("HttpOnly");
|
|
58
|
+
if (options.secure) parts.push("Secure");
|
|
59
|
+
return parts.join("; ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function expireCookie(name: string, path: string): string {
|
|
63
|
+
return `${name}=; Path=${path}; Max-Age=0; SameSite=Lax; HttpOnly`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readCookie(request: Request, name: string): string | undefined {
|
|
67
|
+
const header = request.headers.get("cookie");
|
|
68
|
+
if (!header) return undefined;
|
|
69
|
+
for (const part of header.split(";")) {
|
|
70
|
+
const [rawName, ...rest] = part.trim().split("=");
|
|
71
|
+
if (rawName === name) return decodeURIComponent(rest.join("="));
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The last path segment names the leg: ".../authorize" or ".../callback". */
|
|
77
|
+
export function routeFor(url: string): string {
|
|
78
|
+
const pathname = new URL(url).pathname.replace(/\/+$/, "");
|
|
79
|
+
return pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createOneConnectRoutes(
|
|
83
|
+
oneConnect: OneConnect,
|
|
84
|
+
options: OneConnectRoutesOptions,
|
|
85
|
+
): OneConnectRoutes {
|
|
86
|
+
const cookiePath = (): string =>
|
|
87
|
+
oneConnect.startAuthorization().cookie.options.path;
|
|
88
|
+
|
|
89
|
+
const requireUser = async (request: Request): Promise<string | Response> => {
|
|
90
|
+
const userId = await options.identifyUser(request);
|
|
91
|
+
if (userId) return userId;
|
|
92
|
+
if (options.signInUrl)
|
|
93
|
+
return redirectWithCookies(new URL(options.signInUrl, request.url).toString(), []);
|
|
94
|
+
return new Response("Sign in to your account before connecting One.", {
|
|
95
|
+
status: 401,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const authorize = async (request: Request): Promise<Response> => {
|
|
100
|
+
const user = await requireUser(request);
|
|
101
|
+
if (user instanceof Response) return user;
|
|
102
|
+
const loginHint = (await options.loginHintFor?.(request)) ?? undefined;
|
|
103
|
+
const { redirectUrl, cookie } = oneConnect.startAuthorization({ loginHint });
|
|
104
|
+
return redirectWithCookies(redirectUrl, [
|
|
105
|
+
serializeCookie(cookie.name, cookie.value, cookie.options),
|
|
106
|
+
]);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const callback = async (request: Request): Promise<Response> => {
|
|
110
|
+
const user = await requireUser(request);
|
|
111
|
+
if (user instanceof Response) return user;
|
|
112
|
+
const result = await oneConnect.completeAuthorization({
|
|
113
|
+
userId: user,
|
|
114
|
+
url: request.url,
|
|
115
|
+
getCookie: (name) => readCookie(request, name),
|
|
116
|
+
});
|
|
117
|
+
return redirectWithCookies(
|
|
118
|
+
result.redirectUrl,
|
|
119
|
+
result.clearCookieName
|
|
120
|
+
? [expireCookie(result.clearCookieName, cookiePath())]
|
|
121
|
+
: [],
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
GET: async (request: Request): Promise<Response> => {
|
|
127
|
+
const route = routeFor(request.url);
|
|
128
|
+
if (route === AUTHORIZE_ROUTE) return authorize(request);
|
|
129
|
+
if (route === CALLBACK_ROUTE) return callback(request);
|
|
130
|
+
return new Response("Not found", { status: 404 });
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|