@withone/connect 0.8.3 → 0.10.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 +156 -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 +78 -0
- package/dist/useOneConnect.d.ts +13 -2
- package/dist/vue.cjs.js +1 -1
- package/dist/vue.d.ts +19 -26
- package/dist/vue.esm.js +1 -1
- package/dist/wrapper-options.d.ts +20 -16
- package/package.json +37 -15
- package/skills/one-connect/SKILL.md +194 -0
- package/src/button.ts +115 -167
- 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 +84 -0
- package/src/useOneConnect.ts +36 -69
- package/src/vue.ts +23 -30
- package/src/wrapper-options.ts +45 -25
- 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/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
|
+
}
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@withone/connect/node`: the two routes as handlers for Node's
|
|
3
|
+
* `http` request and response objects, which is what Express, Fastify
|
|
4
|
+
* (via `reply.raw`), Koa (via `ctx.req`/`ctx.res`) and plain
|
|
5
|
+
* `http.createServer` hand you.
|
|
6
|
+
*
|
|
7
|
+
* import express from "express";
|
|
8
|
+
* import { createOneConnectHandlers } from "@withone/connect/node";
|
|
9
|
+
* import { oneConnect } from "./one";
|
|
10
|
+
*
|
|
11
|
+
* const { authorize, callback } = createOneConnectHandlers(oneConnect, {
|
|
12
|
+
* identifyUser: (request) => request.session?.userId ?? null,
|
|
13
|
+
* });
|
|
14
|
+
* app.get("/api/one/authorize", authorize);
|
|
15
|
+
* app.get("/api/one/callback", callback);
|
|
16
|
+
*/
|
|
17
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
|
+
|
|
19
|
+
import { createOneConnectRoutes, type OneConnectRoutesOptions } from "@withone/connect/next";
|
|
20
|
+
import type { OneConnect } from "./server";
|
|
21
|
+
|
|
22
|
+
export interface OneConnectNodeOptions {
|
|
23
|
+
/** The app's own id for the signed-in user, or null when nobody is
|
|
24
|
+
* signed in. */
|
|
25
|
+
identifyUser: (
|
|
26
|
+
request: IncomingMessage,
|
|
27
|
+
) => Promise<string | null> | string | null;
|
|
28
|
+
/** Pre-fills the sign-in email on One's page for this user. */
|
|
29
|
+
loginHintFor?: (
|
|
30
|
+
request: IncomingMessage,
|
|
31
|
+
) => Promise<string | null> | string | null;
|
|
32
|
+
/** Where to send the browser when nobody is signed in. */
|
|
33
|
+
signInUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type NodeHandler = (
|
|
37
|
+
request: IncomingMessage,
|
|
38
|
+
response: ServerResponse,
|
|
39
|
+
) => Promise<void>;
|
|
40
|
+
|
|
41
|
+
export interface OneConnectHandlers {
|
|
42
|
+
authorize: NodeHandler;
|
|
43
|
+
callback: NodeHandler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The incoming request as a web Request, so both adapters share one
|
|
47
|
+
* implementation. */
|
|
48
|
+
function toWebRequest(request: IncomingMessage): Request {
|
|
49
|
+
const protocol =
|
|
50
|
+
request.headers["x-forwarded-proto"]?.toString().split(",")[0] ??
|
|
51
|
+
((request.socket as { encrypted?: boolean }).encrypted ? "https" : "http");
|
|
52
|
+
const host = request.headers["x-forwarded-host"] ?? request.headers.host ?? "localhost";
|
|
53
|
+
const headers = new Headers();
|
|
54
|
+
for (const [name, value] of Object.entries(request.headers)) {
|
|
55
|
+
if (typeof value === "string") headers.set(name, value);
|
|
56
|
+
else if (Array.isArray(value)) headers.set(name, value.join(", "));
|
|
57
|
+
}
|
|
58
|
+
return new Request(`${protocol}://${host}${request.url ?? "/"}`, {
|
|
59
|
+
method: request.method ?? "GET",
|
|
60
|
+
headers,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function send(response: ServerResponse, web: Response): Promise<void> {
|
|
65
|
+
const setCookies =
|
|
66
|
+
typeof (web.headers as Headers & { getSetCookie?: () => string[] })
|
|
67
|
+
.getSetCookie === "function"
|
|
68
|
+
? (web.headers as Headers & { getSetCookie: () => string[] }).getSetCookie()
|
|
69
|
+
: [];
|
|
70
|
+
web.headers.forEach((value, name) => {
|
|
71
|
+
if (name.toLowerCase() !== "set-cookie") response.setHeader(name, value);
|
|
72
|
+
});
|
|
73
|
+
if (setCookies.length > 0) response.setHeader("Set-Cookie", setCookies);
|
|
74
|
+
response.statusCode = web.status;
|
|
75
|
+
response.end(await web.text());
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createOneConnectHandlers(
|
|
79
|
+
oneConnect: OneConnect,
|
|
80
|
+
options: OneConnectNodeOptions,
|
|
81
|
+
): OneConnectHandlers {
|
|
82
|
+
// The web adapter receives a Request; the Node callbacks want the
|
|
83
|
+
// original IncomingMessage, so it is carried alongside by closure.
|
|
84
|
+
let current: IncomingMessage | null = null;
|
|
85
|
+
const routeOptions: OneConnectRoutesOptions = {
|
|
86
|
+
identifyUser: () => options.identifyUser(current as IncomingMessage),
|
|
87
|
+
loginHintFor: options.loginHintFor
|
|
88
|
+
? () => options.loginHintFor!(current as IncomingMessage)
|
|
89
|
+
: undefined,
|
|
90
|
+
signInUrl: options.signInUrl,
|
|
91
|
+
};
|
|
92
|
+
const routes = createOneConnectRoutes(oneConnect, routeOptions);
|
|
93
|
+
|
|
94
|
+
const handle = async (
|
|
95
|
+
request: IncomingMessage,
|
|
96
|
+
response: ServerResponse,
|
|
97
|
+
): Promise<void> => {
|
|
98
|
+
current = request;
|
|
99
|
+
try {
|
|
100
|
+
await send(response, await routes.GET(toWebRequest(request)));
|
|
101
|
+
} finally {
|
|
102
|
+
current = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return { authorize: handle, callback: handle };
|
|
107
|
+
}
|
package/src/platforms.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { CONNECTOR_ASSETS_URL } from "./constants";
|
|
2
|
+
import type {
|
|
3
|
+
ConnectButtonPlatform,
|
|
4
|
+
ConnectButtonPlatformInput,
|
|
5
|
+
} from "./types";
|
|
6
|
+
|
|
7
|
+
/** "Google Calendar" -> "google-calendar". */
|
|
8
|
+
export function slugFromName(name: string): string {
|
|
9
|
+
return name
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
13
|
+
.replace(/^-+|-+$/g, "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** "google-calendar" -> "Google Calendar". */
|
|
17
|
+
export function nameFromSlug(slug: string): string {
|
|
18
|
+
return slug
|
|
19
|
+
.split(/[-_]+/)
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
22
|
+
.join(" ");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One's logo for a connector slug. */
|
|
26
|
+
export function logoUrlForSlug(slug: string): string {
|
|
27
|
+
return slug ? `${CONNECTOR_ASSETS_URL}/${slug}.svg` : "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Turns whatever the developer passed into what the button draws. A bare
|
|
32
|
+
* string is a slug; an object may carry any of slug, name and imageUrl.
|
|
33
|
+
* Entries with neither a slug nor a name are dropped rather than drawn
|
|
34
|
+
* as an empty chip.
|
|
35
|
+
*/
|
|
36
|
+
export function normalizePlatform(
|
|
37
|
+
input: ConnectButtonPlatformInput,
|
|
38
|
+
): ConnectButtonPlatform | null {
|
|
39
|
+
const raw = typeof input === "string" ? { slug: input } : input;
|
|
40
|
+
const slug = raw.slug?.trim()
|
|
41
|
+
? slugFromName(raw.slug)
|
|
42
|
+
: raw.name?.trim()
|
|
43
|
+
? slugFromName(raw.name)
|
|
44
|
+
: "";
|
|
45
|
+
if (!slug) return null;
|
|
46
|
+
return {
|
|
47
|
+
slug,
|
|
48
|
+
name: raw.name?.trim() || nameFromSlug(slug),
|
|
49
|
+
imageUrl: raw.imageUrl?.trim() || logoUrlForSlug(slug),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizePlatforms(
|
|
54
|
+
inputs: ConnectButtonPlatformInput[] | undefined,
|
|
55
|
+
): ConnectButtonPlatform[] {
|
|
56
|
+
return (inputs ?? [])
|
|
57
|
+
.map(normalizePlatform)
|
|
58
|
+
.filter((p): p is ConnectButtonPlatform => p !== null);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The `platforms` attribute on <one-connect-button>: a comma list of
|
|
63
|
+
* slugs ("stripe, notion") or, for overrides, a JSON array of
|
|
64
|
+
* {slug, name, imageUrl} objects.
|
|
65
|
+
*/
|
|
66
|
+
export function parsePlatformsAttribute(
|
|
67
|
+
raw: string | null,
|
|
68
|
+
): ConnectButtonPlatformInput[] {
|
|
69
|
+
const value = raw?.trim();
|
|
70
|
+
if (!value) return [];
|
|
71
|
+
if (value.startsWith("[")) {
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(value) as unknown;
|
|
74
|
+
return Array.isArray(parsed)
|
|
75
|
+
? parsed.filter(
|
|
76
|
+
(entry): entry is ConnectButtonPlatformInput =>
|
|
77
|
+
typeof entry === "string" ||
|
|
78
|
+
(typeof entry === "object" && entry !== null),
|
|
79
|
+
)
|
|
80
|
+
: [];
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return value
|
|
86
|
+
.split(",")
|
|
87
|
+
.map((slug) => slug.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* React
|
|
2
|
+
* React: `import { ConnectButton } from "@withone/connect/react"`.
|
|
3
3
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the core package stays dependency-free for every other framework.
|
|
4
|
+
* A component over the framework-agnostic core, so React apps get real
|
|
5
|
+
* props (arrays, functions) instead of the custom element's string
|
|
6
|
+
* attributes. React is a peer dependency of this subpath only.
|
|
8
7
|
*/
|
|
9
8
|
import { createElement, useEffect, useRef } from "react";
|
|
10
9
|
import type { CSSProperties, ReactElement } from "react";
|
|
11
10
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
import {
|
|
12
|
+
mountConnectButton,
|
|
13
|
+
optionsFromProps,
|
|
14
|
+
propsIdentity,
|
|
15
|
+
type ConnectButtonHandle,
|
|
16
|
+
type ConnectButtonProps as ConnectButtonCoreProps,
|
|
17
|
+
} from "@withone/connect";
|
|
18
18
|
|
|
19
|
-
export interface ConnectButtonProps {
|
|
20
|
-
/** The app's own backend authorize route ("/api/one/authorize" is
|
|
21
|
-
* fine — relative resolves against the page). */
|
|
22
|
-
authorizeUrl: string;
|
|
23
|
-
/** Theme for One's hosted page (rides the URL fragment). */
|
|
24
|
-
appTheme?: "light" | "dark";
|
|
25
|
-
onSuccess?: () => void;
|
|
26
|
-
onError?: (error: string) => void;
|
|
27
|
-
onClose?: () => void;
|
|
28
|
-
label?: string;
|
|
29
|
-
variant?: "default" | "accent" | "block";
|
|
30
|
-
/** Matches the host page, not One's card. */
|
|
31
|
-
theme?: "light" | "dark";
|
|
32
|
-
platforms?: ConnectButtonPlatform[];
|
|
33
|
-
moreCount?: number;
|
|
34
|
-
description?: string;
|
|
35
|
-
accentColor?: string;
|
|
36
|
-
connectedLabel?: string;
|
|
19
|
+
export interface ConnectButtonProps extends ConnectButtonCoreProps {
|
|
37
20
|
className?: string;
|
|
38
21
|
style?: CSSProperties;
|
|
39
22
|
}
|
|
@@ -46,54 +29,28 @@ export function ConnectButton(props: ConnectButtonProps): ReactElement {
|
|
|
46
29
|
const callbacksRef = useRef({
|
|
47
30
|
onSuccess: props.onSuccess,
|
|
48
31
|
onError: props.onError,
|
|
49
|
-
onClose: props.onClose,
|
|
50
32
|
});
|
|
51
|
-
callbacksRef.current = {
|
|
52
|
-
onSuccess: props.onSuccess,
|
|
53
|
-
onError: props.onError,
|
|
54
|
-
onClose: props.onClose,
|
|
55
|
-
};
|
|
33
|
+
callbacksRef.current = { onSuccess: props.onSuccess, onError: props.onError };
|
|
56
34
|
|
|
57
|
-
const
|
|
35
|
+
const identity = propsIdentity(props);
|
|
58
36
|
|
|
59
37
|
useEffect(() => {
|
|
60
38
|
if (!containerRef.current) return;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
appTheme: props.appTheme,
|
|
39
|
+
handleRef.current = mountConnectButton(
|
|
40
|
+
containerRef.current,
|
|
41
|
+
optionsFromProps(props, {
|
|
65
42
|
onSuccess: () => callbacksRef.current.onSuccess?.(),
|
|
66
|
-
onError: (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
label: props.label,
|
|
70
|
-
variant: props.variant,
|
|
71
|
-
theme: props.theme,
|
|
72
|
-
platforms: props.platforms,
|
|
73
|
-
moreCount: props.moreCount,
|
|
74
|
-
description: props.description,
|
|
75
|
-
accentColor: props.accentColor,
|
|
76
|
-
connectedLabel: props.connectedLabel,
|
|
77
|
-
};
|
|
78
|
-
handleRef.current = mountConnectButton(containerRef.current, options);
|
|
43
|
+
onError: (message) => callbacksRef.current.onError?.(message),
|
|
44
|
+
}),
|
|
45
|
+
);
|
|
79
46
|
return () => {
|
|
80
47
|
handleRef.current?.destroy();
|
|
81
48
|
handleRef.current = null;
|
|
82
49
|
};
|
|
83
|
-
// Remount only when
|
|
50
|
+
// Remount only when a visual or config input changes; `identity`
|
|
51
|
+
// captures exactly those.
|
|
84
52
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
85
|
-
}, [
|
|
86
|
-
props.authorizeUrl,
|
|
87
|
-
props.appTheme,
|
|
88
|
-
props.label,
|
|
89
|
-
props.variant,
|
|
90
|
-
props.theme,
|
|
91
|
-
platformsKey,
|
|
92
|
-
props.moreCount,
|
|
93
|
-
props.description,
|
|
94
|
-
props.accentColor,
|
|
95
|
-
props.connectedLabel,
|
|
96
|
-
]);
|
|
53
|
+
}, [identity]);
|
|
97
54
|
|
|
98
55
|
return createElement("div", {
|
|
99
56
|
ref: containerRef,
|
package/src/return.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { RETURN_MESSAGE_PARAM, RETURN_STATUS_PARAM } from "./constants";
|
|
2
|
+
import type { OneConnectReturn } from "./types";
|
|
3
|
+
|
|
4
|
+
/** Reads the outcome the callback route put on the return URL. */
|
|
5
|
+
export function parseReturn(search: string): OneConnectReturn | null {
|
|
6
|
+
const params = new URLSearchParams(search);
|
|
7
|
+
const status = params.get(RETURN_STATUS_PARAM);
|
|
8
|
+
if (status !== "success" && status !== "error") return null;
|
|
9
|
+
const message = params.get(RETURN_MESSAGE_PARAM) ?? undefined;
|
|
10
|
+
return message ? { status, message } : { status };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The same URL without the return params, so a refresh does not
|
|
14
|
+
* re-fire the callbacks. */
|
|
15
|
+
export function stripReturnParams(location: {
|
|
16
|
+
pathname: string;
|
|
17
|
+
search: string;
|
|
18
|
+
hash: string;
|
|
19
|
+
}): string {
|
|
20
|
+
const params = new URLSearchParams(location.search);
|
|
21
|
+
params.delete(RETURN_STATUS_PARAM);
|
|
22
|
+
params.delete(RETURN_MESSAGE_PARAM);
|
|
23
|
+
const query = params.toString();
|
|
24
|
+
return `${location.pathname}${query ? `?${query}` : ""}${location.hash}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function hasReturnParams(search: string): boolean {
|
|
28
|
+
const params = new URLSearchParams(search);
|
|
29
|
+
return params.has(RETURN_STATUS_PARAM) || params.has(RETURN_MESSAGE_PARAM);
|
|
30
|
+
}
|