@withone/connect 0.7.0 → 0.8.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 +16 -30
- package/dist/constants.d.ts +2 -8
- package/dist/index.d.ts +0 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/react.cjs.js +1 -1
- package/dist/react.d.ts +1 -3
- package/dist/react.esm.js +1 -1
- package/dist/svelte.cjs.js +1 -1
- package/dist/svelte.esm.js +1 -1
- package/dist/types/index.d.ts +18 -32
- package/dist/vue.cjs.js +1 -1
- package/dist/vue.esm.js +1 -1
- package/dist/wrapper-options.d.ts +0 -1
- package/package.json +2 -2
- package/src/button.ts +0 -4
- package/src/constants.ts +2 -15
- package/src/index.ts +0 -1
- package/src/react-types.d.ts +0 -1
- package/src/react.ts +1 -5
- package/src/svelte-types.d.ts +0 -1
- package/src/types/index.d.ts +18 -32
- package/src/useOneConnect.ts +58 -141
- package/src/wrapper-options.ts +0 -2
- package/dist/complete.d.ts +0 -12
- package/dist/window/index.d.ts +0 -15
- package/src/complete.ts +0 -36
- package/src/window/index.ts +0 -171
package/src/useOneConnect.ts
CHANGED
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
-
EXIT_MESSAGE_TYPE,
|
|
3
|
-
MESSAGE_TYPE,
|
|
4
2
|
RETURN_MESSAGE_PARAM,
|
|
5
3
|
RETURN_STATUS_PARAM,
|
|
6
4
|
THEME_PARAM,
|
|
7
5
|
} from "./constants";
|
|
8
|
-
import {
|
|
9
|
-
createEmbedIframe,
|
|
10
|
-
getEmbedIframe,
|
|
11
|
-
removeEmbedIframe,
|
|
12
|
-
removeSuccessOverlay,
|
|
13
|
-
showErrorOverlay,
|
|
14
|
-
showSuccessOverlay,
|
|
15
|
-
} from "./window";
|
|
16
|
-
import type {
|
|
17
|
-
OneConnectHandle,
|
|
18
|
-
OneConnectMessage,
|
|
19
|
-
OneConnectProps,
|
|
20
|
-
} from "./types";
|
|
6
|
+
import type { OneConnectHandle, OneConnectProps } from "./types";
|
|
21
7
|
|
|
22
8
|
// Like useOneAuth, this is a plain function rather than a React hook so
|
|
23
9
|
// it works from any framework.
|
|
24
10
|
|
|
25
|
-
/** The return leg is a page LOAD
|
|
26
|
-
*
|
|
11
|
+
/** The return leg is a page LOAD — only the first hook instance on the
|
|
12
|
+
* page consumes it. */
|
|
27
13
|
let returnConsumed = false;
|
|
28
14
|
|
|
29
15
|
export const useOneConnect = (props: OneConnectProps): OneConnectHandle => {
|
|
30
|
-
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
31
16
|
let resultDelivered = false;
|
|
32
|
-
const mode = props.mode ?? "redirect";
|
|
33
17
|
|
|
34
18
|
// The theme rides in the URL FRAGMENT: fragments never reach any
|
|
35
19
|
// server and browsers carry them through the whole redirect chain
|
|
36
|
-
// (consumer's authorize route -> One -> the
|
|
37
|
-
// backend forwards NOTHING.
|
|
38
|
-
// card detects its own iframe with window.self !== window.top.
|
|
20
|
+
// (consumer's authorize route -> One -> the hosted page), so the
|
|
21
|
+
// consumer's backend forwards NOTHING.
|
|
39
22
|
const buildUrl = (): string => {
|
|
40
23
|
try {
|
|
41
24
|
// Relative paths ("/api/one/authorize") resolve against the host
|
|
@@ -51,136 +34,70 @@ export const useOneConnect = (props: OneConnectProps): OneConnectHandle => {
|
|
|
51
34
|
}
|
|
52
35
|
};
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
removeEmbedIframe();
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const deliver = (status: "success" | "error", message?: string) => {
|
|
63
|
-
if (resultDelivered) return;
|
|
64
|
-
resultDelivered = true;
|
|
65
|
-
teardown();
|
|
66
|
-
// The confirmation beat: by now the frame is gone, so the SDK
|
|
67
|
-
// paints the result card itself — success AND error both live in
|
|
68
|
-
// the widget surface (same as authkit), never only in the host UI.
|
|
69
|
-
if (status === "success") showSuccessOverlay(props.appTheme);
|
|
70
|
-
else showErrorOverlay(props.appTheme, message);
|
|
71
|
-
try {
|
|
72
|
-
if (status === "success") {
|
|
73
|
-
props.onSuccess?.();
|
|
74
|
-
} else {
|
|
75
|
-
props.onError?.(message ?? "The connection was not completed.");
|
|
76
|
-
}
|
|
77
|
-
} catch {
|
|
78
|
-
/* consumer callback errors are not our problem */
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Only trust messages from OUR iframe's browsing context. Exit can
|
|
83
|
-
// come from One's page (cross-origin); results come from the
|
|
84
|
-
// completion page, which is the consumer's own origin because the
|
|
85
|
-
// OAuth redirect brought the frame home.
|
|
86
|
-
const handleMessage = (event: MessageEvent) => {
|
|
87
|
-
const data = event.data as OneConnectMessage | undefined;
|
|
88
|
-
if (!data) return;
|
|
89
|
-
|
|
90
|
-
const iframe = getEmbedIframe();
|
|
91
|
-
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
92
|
-
|
|
93
|
-
if (data.type === EXIT_MESSAGE_TYPE) {
|
|
94
|
-
teardown();
|
|
95
|
-
try {
|
|
96
|
-
props.onClose?.();
|
|
97
|
-
} catch {
|
|
98
|
-
/* ignore */
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (
|
|
103
|
-
data.type === MESSAGE_TYPE &&
|
|
104
|
-
event.origin === window.location.origin &&
|
|
105
|
-
(data.status === "success" || data.status === "error")
|
|
106
|
-
) {
|
|
107
|
-
deliver(data.status, data.message);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
// Fires on every navigation inside the frame. While the frame is on
|
|
112
|
-
// One's origin, reading its location throws (same-origin policy) and
|
|
113
|
-
// we ignore it. The moment the consumer's callback redirects home —
|
|
114
|
-
// to ANY same-origin URL carrying ?one_connect=success|error — the
|
|
115
|
-
// read succeeds and the flow completes. The consumer writes no
|
|
116
|
-
// completion page and no postMessage; their callback's final
|
|
117
|
-
// redirect IS the completion signal.
|
|
118
|
-
const handleFrameLoad = () => {
|
|
119
|
-
const iframe = getEmbedIframe();
|
|
120
|
-
if (!iframe) return;
|
|
121
|
-
let href: string;
|
|
122
|
-
try {
|
|
123
|
-
href = iframe.contentWindow?.location.href ?? "";
|
|
124
|
-
} catch {
|
|
125
|
-
return; // still cross-origin — not home yet
|
|
126
|
-
}
|
|
127
|
-
let params: URLSearchParams;
|
|
128
|
-
try {
|
|
129
|
-
params = new URL(href).searchParams;
|
|
130
|
-
} catch {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const status = params.get(RETURN_STATUS_PARAM);
|
|
134
|
-
if (status !== "success" && status !== "error") return;
|
|
135
|
-
iframe.style.visibility = "hidden"; // no flash of the landing page
|
|
136
|
-
deliver(status, params.get(RETURN_MESSAGE_PARAM) ?? undefined);
|
|
137
|
-
};
|
|
138
|
-
|
|
37
|
+
// Full-page hosted flow, Stripe-Checkout style: same tab, One's own
|
|
38
|
+
// domain (first-party cookies — works in every browser). The app's
|
|
39
|
+
// callback redirect brings the user home; detection below picks it
|
|
40
|
+
// up on the next load.
|
|
139
41
|
const open = () => {
|
|
140
42
|
if (typeof window === "undefined") return;
|
|
141
|
-
|
|
142
|
-
// Full-page hosted flow, Stripe-Checkout style: same tab, One's
|
|
143
|
-
// own domain (first-party — works in every browser). The app's
|
|
144
|
-
// callback redirect brings the user home; detection below picks
|
|
145
|
-
// it up on the next load.
|
|
146
|
-
window.location.assign(buildUrl());
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
resultDelivered = false;
|
|
150
|
-
|
|
151
|
-
messageHandler = handleMessage;
|
|
152
|
-
window.addEventListener("message", messageHandler);
|
|
153
|
-
const iframe = createEmbedIframe(buildUrl());
|
|
154
|
-
iframe.addEventListener("load", handleFrameLoad);
|
|
43
|
+
window.location.assign(buildUrl());
|
|
155
44
|
};
|
|
156
45
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// state, and that unmount must not eat the confirmation. The
|
|
161
|
-
// overlay dismisses only via its own controls (X / Close / Esc /
|
|
162
|
-
// scrim) unless the host explicitly closes everything.
|
|
163
|
-
if (!options?.keepResult) removeSuccessOverlay();
|
|
164
|
-
teardown();
|
|
165
|
-
};
|
|
46
|
+
// Nothing to dismantle in the full-page flow — kept so hosts (and the
|
|
47
|
+
// button's destroy path) can call it unconditionally.
|
|
48
|
+
const close = (_options?: { keepResult?: boolean }) => {};
|
|
166
49
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
50
|
+
// Return detection: the app's callback redirected the TAB to a URL
|
|
51
|
+
// carrying ?one_connect=… — deliver it once and scrub the params so a
|
|
52
|
+
// refresh doesn't re-fire.
|
|
170
53
|
if (typeof window !== "undefined" && !returnConsumed) {
|
|
171
54
|
const search = new URLSearchParams(window.location.search);
|
|
172
55
|
const returnStatus = search.get(RETURN_STATUS_PARAM);
|
|
173
56
|
if (returnStatus === "success" || returnStatus === "error") {
|
|
174
57
|
returnConsumed = true;
|
|
175
58
|
const returnMessage = search.get(RETURN_MESSAGE_PARAM) ?? undefined;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
59
|
+
// Scrub-and-verify: this code runs during the app's first render,
|
|
60
|
+
// and frameworks that manage the history themselves (Next.js App
|
|
61
|
+
// Router) sync the address bar back to THEIR canonical URL when
|
|
62
|
+
// hydration completes — silently undoing a one-shot replaceState.
|
|
63
|
+
// So scrub now, then re-check a few beats later and scrub again if
|
|
64
|
+
// the params were restored.
|
|
65
|
+
const scrub = () => {
|
|
66
|
+
const params = new URLSearchParams(window.location.search);
|
|
67
|
+
if (
|
|
68
|
+
!params.has(RETURN_STATUS_PARAM) &&
|
|
69
|
+
!params.has(RETURN_MESSAGE_PARAM)
|
|
70
|
+
)
|
|
71
|
+
return;
|
|
72
|
+
params.delete(RETURN_STATUS_PARAM);
|
|
73
|
+
params.delete(RETURN_MESSAGE_PARAM);
|
|
74
|
+
window.history.replaceState(
|
|
75
|
+
null,
|
|
76
|
+
"",
|
|
77
|
+
window.location.pathname +
|
|
78
|
+
(params.toString() ? `?${params.toString()}` : "") +
|
|
79
|
+
window.location.hash,
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
scrub();
|
|
83
|
+
for (const delay of [50, 500, 2000]) window.setTimeout(scrub, delay);
|
|
84
|
+
// No overlay: One's HOSTED page already showed the result beat
|
|
85
|
+
// ("You're all set" + countdown / the failure screen) before
|
|
86
|
+
// sending the user home — painting a second card would
|
|
87
|
+
// double-announce. Callbacks fire so the app updates its state.
|
|
88
|
+
window.setTimeout(() => {
|
|
89
|
+
if (resultDelivered) return;
|
|
90
|
+
resultDelivered = true;
|
|
91
|
+
try {
|
|
92
|
+
if (returnStatus === "success") props.onSuccess?.();
|
|
93
|
+
else
|
|
94
|
+
props.onError?.(
|
|
95
|
+
returnMessage ?? "The connection was not completed.",
|
|
96
|
+
);
|
|
97
|
+
} catch {
|
|
98
|
+
/* consumer callback errors are not our problem */
|
|
99
|
+
}
|
|
100
|
+
}, 0);
|
|
184
101
|
}
|
|
185
102
|
}
|
|
186
103
|
|
package/src/wrapper-options.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ConnectButtonOptions, ConnectButtonPlatform } from "./types";
|
|
|
5
5
|
* the core mountConnectButton options. */
|
|
6
6
|
export interface ConnectButtonWrapperProps {
|
|
7
7
|
authorizeUrl: string;
|
|
8
|
-
mode?: "redirect" | "modal";
|
|
9
8
|
appTheme?: "light" | "dark";
|
|
10
9
|
onSuccess?: () => void;
|
|
11
10
|
onError?: (error: string) => void;
|
|
@@ -33,7 +32,6 @@ export function optionsFromWrapperProps(
|
|
|
33
32
|
connect: {
|
|
34
33
|
authorize: { url: props.authorizeUrl },
|
|
35
34
|
appTheme: props.appTheme,
|
|
36
|
-
mode: props.mode,
|
|
37
35
|
onSuccess: () => cb.onSuccess?.(),
|
|
38
36
|
onError: (error) => cb.onError?.(error),
|
|
39
37
|
onClose: () => cb.onClose?.(),
|
package/dist/complete.d.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { OneConnectResult } from "./types";
|
|
2
|
-
/**
|
|
3
|
-
* OPTIONAL. The standard integration needs no completion page at all:
|
|
4
|
-
* the callback route's final redirect carries ?one_connect=success (or
|
|
5
|
-
* error) on any same-origin URL, and the SDK reads it off the frame's
|
|
6
|
-
* location directly. Use this helper only if you render a custom
|
|
7
|
-
* completion page and want to signal the SDK from it explicitly.
|
|
8
|
-
*
|
|
9
|
-
* Returns false when it had nothing to do (not inside a frame) — e.g.
|
|
10
|
-
* the user opened the page directly. Render fallback UI in that case.
|
|
11
|
-
*/
|
|
12
|
-
export declare function completeOneConnect(result?: OneConnectResult): boolean;
|
package/dist/window/index.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export declare const IFRAME_ID = "one-connect-frame";
|
|
2
|
-
export declare function createEmbedIframe(url: string): HTMLIFrameElement;
|
|
3
|
-
export declare function removeEmbedIframe(): void;
|
|
4
|
-
export declare const SUCCESS_ID = "one-connect-success";
|
|
5
|
-
/** The result confirmation shown after the flow ends — the same beat as
|
|
6
|
-
* authkit's in-frame result screens (success AND error both render in
|
|
7
|
-
* the widget surface, never only in the host page), dismissed the same
|
|
8
|
-
* way: the user closes it with the ✕ or the Close button, never a
|
|
9
|
-
* timer. By this point the card's iframe has already navigated home
|
|
10
|
-
* and been removed, so the SDK paints this itself. Pure inline styles
|
|
11
|
-
* — the SDK ships no CSS and loads no assets. */
|
|
12
|
-
export declare function showSuccessOverlay(theme?: "dark" | "light"): void;
|
|
13
|
-
export declare function showErrorOverlay(theme?: "dark" | "light", message?: string): void;
|
|
14
|
-
export declare function removeSuccessOverlay(): void;
|
|
15
|
-
export declare function getEmbedIframe(): HTMLIFrameElement | null;
|
package/src/complete.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { MESSAGE_TYPE } from "./constants";
|
|
2
|
-
import type { OneConnectMessage, OneConnectResult } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* OPTIONAL. The standard integration needs no completion page at all:
|
|
6
|
-
* the callback route's final redirect carries ?one_connect=success (or
|
|
7
|
-
* error) on any same-origin URL, and the SDK reads it off the frame's
|
|
8
|
-
* location directly. Use this helper only if you render a custom
|
|
9
|
-
* completion page and want to signal the SDK from it explicitly.
|
|
10
|
-
*
|
|
11
|
-
* Returns false when it had nothing to do (not inside a frame) — e.g.
|
|
12
|
-
* the user opened the page directly. Render fallback UI in that case.
|
|
13
|
-
*/
|
|
14
|
-
export function completeOneConnect(
|
|
15
|
-
result: OneConnectResult = { status: "success" }
|
|
16
|
-
): boolean {
|
|
17
|
-
if (typeof window === "undefined") return false;
|
|
18
|
-
|
|
19
|
-
if (window.parent && window.parent !== window) {
|
|
20
|
-
const message: OneConnectMessage = {
|
|
21
|
-
type: MESSAGE_TYPE,
|
|
22
|
-
status: result.status,
|
|
23
|
-
message: result.message,
|
|
24
|
-
};
|
|
25
|
-
try {
|
|
26
|
-
// Target the consumer origin only — never "*". The host page
|
|
27
|
-
// additionally checks event.origin before trusting the message.
|
|
28
|
-
window.parent.postMessage(message, window.location.origin);
|
|
29
|
-
return true;
|
|
30
|
-
} catch {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return false;
|
|
36
|
-
}
|
package/src/window/index.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
// Frame management for @withone/connect.
|
|
2
|
-
//
|
|
3
|
-
// The SDK has exactly one presentation: an authkit-style modal. A
|
|
4
|
-
// full-viewport transparent iframe sits over the host page; One's
|
|
5
|
-
// connect page renders a scrim + centered card inside it, so the host
|
|
6
|
-
// app stays visible and dimmed underneath. Works at every viewport
|
|
7
|
-
// size — the card is responsive and the frame is the viewport.
|
|
8
|
-
//
|
|
9
|
-
// Transport note: every step of the flow rides on the user's One
|
|
10
|
-
// session cookie, which is a THIRD-PARTY cookie when the host page is
|
|
11
|
-
// on a different site than One. Production embedding therefore relies
|
|
12
|
-
// on One serving that cookie as `Partitioned` (CHIPS) and allowing the
|
|
13
|
-
// client's domain via frame-ancestors (RFC 6749 §10.13). Same-site
|
|
14
|
-
// setups (e.g. localhost dev) work everywhere as-is.
|
|
15
|
-
|
|
16
|
-
export const IFRAME_ID = "one-connect-frame";
|
|
17
|
-
|
|
18
|
-
export function createEmbedIframe(url: string): HTMLIFrameElement {
|
|
19
|
-
removeEmbedIframe();
|
|
20
|
-
const iframe = document.createElement("iframe");
|
|
21
|
-
iframe.id = IFRAME_ID;
|
|
22
|
-
iframe.src = url;
|
|
23
|
-
iframe.setAttribute("allowtransparency", "true");
|
|
24
|
-
Object.assign(iframe.style, {
|
|
25
|
-
position: "fixed",
|
|
26
|
-
inset: "0",
|
|
27
|
-
width: "100%",
|
|
28
|
-
height: "100%",
|
|
29
|
-
border: "0",
|
|
30
|
-
zIndex: "2147483000",
|
|
31
|
-
background: "transparent",
|
|
32
|
-
colorScheme: "normal", // keep the transparent viewport from being painted
|
|
33
|
-
} as Partial<CSSStyleDeclaration>);
|
|
34
|
-
document.body.appendChild(iframe);
|
|
35
|
-
return iframe;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function removeEmbedIframe(): void {
|
|
39
|
-
const existing = document.getElementById(IFRAME_ID);
|
|
40
|
-
if (existing) existing.remove();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export const SUCCESS_ID = "one-connect-success";
|
|
44
|
-
|
|
45
|
-
/** The result confirmation shown after the flow ends — the same beat as
|
|
46
|
-
* authkit's in-frame result screens (success AND error both render in
|
|
47
|
-
* the widget surface, never only in the host page), dismissed the same
|
|
48
|
-
* way: the user closes it with the ✕ or the Close button, never a
|
|
49
|
-
* timer. By this point the card's iframe has already navigated home
|
|
50
|
-
* and been removed, so the SDK paints this itself. Pure inline styles
|
|
51
|
-
* — the SDK ships no CSS and loads no assets. */
|
|
52
|
-
export function showSuccessOverlay(theme?: "dark" | "light"): void {
|
|
53
|
-
showResultOverlay("success", theme);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function showErrorOverlay(
|
|
57
|
-
theme?: "dark" | "light",
|
|
58
|
-
message?: string,
|
|
59
|
-
): void {
|
|
60
|
-
showResultOverlay("error", theme, message);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function showResultOverlay(
|
|
64
|
-
status: "success" | "error",
|
|
65
|
-
theme?: "dark" | "light",
|
|
66
|
-
message?: string,
|
|
67
|
-
): void {
|
|
68
|
-
removeSuccessOverlay();
|
|
69
|
-
const dark = theme === "dark";
|
|
70
|
-
const overlay = document.createElement("div");
|
|
71
|
-
overlay.id = SUCCESS_ID;
|
|
72
|
-
Object.assign(overlay.style, {
|
|
73
|
-
position: "fixed",
|
|
74
|
-
inset: "0",
|
|
75
|
-
zIndex: "2147483000",
|
|
76
|
-
display: "flex",
|
|
77
|
-
alignItems: "center",
|
|
78
|
-
justifyContent: "center",
|
|
79
|
-
background: "rgba(8, 8, 8, 0.5)",
|
|
80
|
-
opacity: "0",
|
|
81
|
-
transition: "opacity 160ms ease",
|
|
82
|
-
} as Partial<CSSStyleDeclaration>);
|
|
83
|
-
|
|
84
|
-
// Same geometry as every other screen in the flow: the authkit card
|
|
85
|
-
// is 520×520 (body) with a 48px "Secured by" strip under it — the
|
|
86
|
-
// success screen must not read as a different, smaller surface.
|
|
87
|
-
const card = document.createElement("div");
|
|
88
|
-
Object.assign(card.style, {
|
|
89
|
-
width: "520px",
|
|
90
|
-
maxWidth: "calc(100vw - 16px)",
|
|
91
|
-
height: "520px",
|
|
92
|
-
maxHeight: "calc(100vh - 16px)",
|
|
93
|
-
display: "flex",
|
|
94
|
-
flexDirection: "column",
|
|
95
|
-
textAlign: "center",
|
|
96
|
-
fontFamily:
|
|
97
|
-
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
98
|
-
} as Partial<CSSStyleDeclaration>);
|
|
99
|
-
const muted = dark ? "#a1a1aa" : "#6b7280";
|
|
100
|
-
const cardBg = dark ? "rgba(25, 25, 25, 0.97)" : "rgba(255, 255, 255, 0.97)";
|
|
101
|
-
const cardBorder = dark ? "rgba(255,255,255,0.08)" : "rgba(228,228,223,0.9)";
|
|
102
|
-
const footerBg = dark ? "rgba(45, 45, 44, 0.9)" : "rgba(232, 232, 228, 0.95)";
|
|
103
|
-
card.innerHTML =
|
|
104
|
-
`<div style="position:relative;flex:1;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;padding:24px 20px;border-radius:28px 28px 0 0;background:${cardBg};border:1px solid ${cardBorder};border-bottom:0;">` +
|
|
105
|
-
`<button data-one-close aria-label="Close" style="position:absolute;top:16px;right:16px;width:20px;height:20px;padding:0;border:0;background:none;cursor:pointer;color:${muted};line-height:0;">` +
|
|
106
|
-
'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>' +
|
|
107
|
-
"</button>" +
|
|
108
|
-
`<div style="width:56px;height:56px;border-radius:50%;background:${status === "success" ? "#10b981" : "#ef4444"};display:flex;align-items:center;justify-content:center;">` +
|
|
109
|
-
(status === "success"
|
|
110
|
-
? '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>'
|
|
111
|
-
: '<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6L6 18M6 6l12 12"/></svg>') +
|
|
112
|
-
"</div>" +
|
|
113
|
-
`<div style="font-size:18px;font-weight:600;letter-spacing:-0.01em;color:${dark ? "#fafafa" : "#111114"};">${status === "success" ? "Access granted" : "Connection failed"}</div>` +
|
|
114
|
-
`<div data-one-result-body style="font-size:13px;line-height:1.5;max-width:280px;color:${muted};"></div>` +
|
|
115
|
-
`<button data-one-close style="width:100%;margin-top:8px;padding:11px 0;border:0;border-radius:12px;cursor:pointer;font-size:14px;font-weight:500;background:${dark ? "#fafafa" : "#111114"};color:${dark ? "#111114" : "#fafafa"};">Close</button>` +
|
|
116
|
-
"</div>" +
|
|
117
|
-
`<div style="height:48px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:6px;border-radius:0 0 28px 28px;background:${footerBg};border:1px solid ${cardBorder};border-top:0;">` +
|
|
118
|
-
`<span style="font-size:11px;color:${muted};">Secured by</span>` +
|
|
119
|
-
`<span style="display:inline-flex;color:${dark ? "#fafafa" : "#111114"};"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="12" cy="12" r="8.5"/></svg></span>` +
|
|
120
|
-
`<span style="font-size:12px;font-weight:600;letter-spacing:-0.02em;color:${dark ? "#fafafa" : "#111114"};">one</span>` +
|
|
121
|
-
"</div>";
|
|
122
|
-
|
|
123
|
-
// Message text arrives from a URL param — set as TEXT, never HTML.
|
|
124
|
-
const body = card.querySelector("[data-one-result-body]");
|
|
125
|
-
if (body)
|
|
126
|
-
body.textContent =
|
|
127
|
-
status === "success"
|
|
128
|
-
? "Your tools are connected. You can pick up right where you left off."
|
|
129
|
-
: (message ??
|
|
130
|
-
"The connection was not completed. You can close this and try again.");
|
|
131
|
-
|
|
132
|
-
overlay.appendChild(card);
|
|
133
|
-
document.body.appendChild(overlay);
|
|
134
|
-
requestAnimationFrame(() => {
|
|
135
|
-
overlay.style.opacity = "1";
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
const dismiss = () => {
|
|
139
|
-
window.removeEventListener("keydown", onKeydown);
|
|
140
|
-
overlay.style.opacity = "0";
|
|
141
|
-
window.setTimeout(() => overlay.remove(), 180);
|
|
142
|
-
};
|
|
143
|
-
function onKeydown(event: KeyboardEvent) {
|
|
144
|
-
if (event.key === "Escape") dismiss();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
for (const button of card.querySelectorAll("[data-one-close]")) {
|
|
148
|
-
button.addEventListener("click", dismiss);
|
|
149
|
-
}
|
|
150
|
-
// The card is dismissed deliberately, never on a timer — but this
|
|
151
|
-
// overlay covers the host page at the top of the stacking context, so
|
|
152
|
-
// it must never be able to strand the app. Clicking the scrim outside
|
|
153
|
-
// the card and pressing Escape are both floors under the buttons: if a
|
|
154
|
-
// button ever fails to render or bind, the user is still not trapped.
|
|
155
|
-
overlay.addEventListener("click", (event) => {
|
|
156
|
-
if (event.target === overlay) dismiss();
|
|
157
|
-
});
|
|
158
|
-
window.addEventListener("keydown", onKeydown);
|
|
159
|
-
|
|
160
|
-
// Send focus somewhere sane for keyboard and screen-reader users, who
|
|
161
|
-
// otherwise land on a full-viewport overlay with no reachable control.
|
|
162
|
-
(card.querySelector("[data-one-close]") as HTMLElement | null)?.focus();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export function removeSuccessOverlay(): void {
|
|
166
|
-
document.getElementById(SUCCESS_ID)?.remove();
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export function getEmbedIframe(): HTMLIFrameElement | null {
|
|
170
|
-
return document.getElementById(IFRAME_ID) as HTMLIFrameElement | null;
|
|
171
|
-
}
|