@wordkey/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +103 -0
- package/dist/chunk-JWCQBSIQ.mjs +13 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +862 -0
- package/dist/index.mjs +821 -0
- package/dist/server.d.mts +48 -0
- package/dist/server.d.ts +48 -0
- package/dist/server.js +106 -0
- package/dist/server.mjs +80 -0
- package/dist/types-Be6QFOjD.d.mts +49 -0
- package/dist/types-Be6QFOjD.d.ts +49 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
WordKeyButton: () => WordKeyButton
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(src_exports);
|
|
36
|
+
|
|
37
|
+
// src/WordKeyButton.tsx
|
|
38
|
+
var import_react = require("react");
|
|
39
|
+
|
|
40
|
+
// src/types.ts
|
|
41
|
+
var FIELD_LABELS = {
|
|
42
|
+
name: "Full name",
|
|
43
|
+
email: "Email address",
|
|
44
|
+
phone: "Phone number",
|
|
45
|
+
shipping_address: "Shipping address"
|
|
46
|
+
};
|
|
47
|
+
var DEFAULT_API_URL = "https://app.wordkey.app";
|
|
48
|
+
|
|
49
|
+
// src/fields.ts
|
|
50
|
+
var STANDARD = {
|
|
51
|
+
name: { key: "name", label: FIELD_LABELS.name, type: "text", isStandard: true },
|
|
52
|
+
email: { key: "email", label: FIELD_LABELS.email, type: "email", isStandard: true },
|
|
53
|
+
phone: { key: "phone", label: FIELD_LABELS.phone, type: "tel", isStandard: true },
|
|
54
|
+
shipping_address: {
|
|
55
|
+
key: "shipping_address",
|
|
56
|
+
label: FIELD_LABELS.shipping_address,
|
|
57
|
+
type: "address",
|
|
58
|
+
isStandard: true
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function resolveFields(requested) {
|
|
62
|
+
return requested.map((f) => {
|
|
63
|
+
if (typeof f === "string") {
|
|
64
|
+
const base = STANDARD[f];
|
|
65
|
+
if (base) return { ...base, required: false };
|
|
66
|
+
return { key: f, label: f, type: "text", required: false, isStandard: false };
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
key: f.key,
|
|
70
|
+
label: f.label,
|
|
71
|
+
type: f.type,
|
|
72
|
+
required: f.required ?? false,
|
|
73
|
+
options: f.options,
|
|
74
|
+
isStandard: false
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/auth.ts
|
|
80
|
+
var PROTOCOL_SALT = "wordkey.v1.protocol.salt";
|
|
81
|
+
async function hashWord(word) {
|
|
82
|
+
const data = new TextEncoder().encode(word.toLowerCase().trim() + PROTOCOL_SALT);
|
|
83
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
84
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
85
|
+
}
|
|
86
|
+
function generateHolderSecret() {
|
|
87
|
+
const bytes = new Uint8Array(32);
|
|
88
|
+
crypto.getRandomValues(bytes);
|
|
89
|
+
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
90
|
+
}
|
|
91
|
+
var DEVICE_STORE = "wordkey.device_id";
|
|
92
|
+
function getDeviceId() {
|
|
93
|
+
if (typeof window === "undefined") return "server";
|
|
94
|
+
try {
|
|
95
|
+
const existing = localStorage.getItem(DEVICE_STORE);
|
|
96
|
+
if (existing) return existing;
|
|
97
|
+
const bytes = new Uint8Array(16);
|
|
98
|
+
crypto.getRandomValues(bytes);
|
|
99
|
+
const id = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
100
|
+
localStorage.setItem(DEVICE_STORE, id);
|
|
101
|
+
return id;
|
|
102
|
+
} catch {
|
|
103
|
+
return "ephemeral";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
var DeviceApprovalRequiredError = class extends Error {
|
|
107
|
+
code = "device_approval_required";
|
|
108
|
+
constructor(message) {
|
|
109
|
+
super(message);
|
|
110
|
+
this.name = "DeviceApprovalRequiredError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
async function verifyWord(apiUrl = DEFAULT_API_URL, publishableKey, word) {
|
|
114
|
+
const wordHash = await hashWord(word);
|
|
115
|
+
const res = await fetch(`${apiUrl}/api/auth/verify`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify({ wordHash, deviceId: getDeviceId(), publishableKey })
|
|
119
|
+
});
|
|
120
|
+
const json = await res.json();
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
if (json?.error?.code === "device_approval_required") {
|
|
123
|
+
throw new DeviceApprovalRequiredError(
|
|
124
|
+
json?.error?.message ?? "Approve this sign-in from your phone."
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
throw new Error(json?.error?.message ?? "Authentication failed");
|
|
128
|
+
}
|
|
129
|
+
return json;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/profile-popup.ts
|
|
133
|
+
var POPUP_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
134
|
+
var PENDING_STORE = "wordkey.pending_share";
|
|
135
|
+
var RESULT_HASH = "#wordkey_result=";
|
|
136
|
+
function b64urlEncode(bytes) {
|
|
137
|
+
let bin = "";
|
|
138
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
139
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
140
|
+
}
|
|
141
|
+
function b64urlDecode(s) {
|
|
142
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
143
|
+
const bin = atob(b64 + "=".repeat((4 - b64.length % 4) % 4));
|
|
144
|
+
const out = new Uint8Array(bin.length);
|
|
145
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function encodeJson(value) {
|
|
149
|
+
return b64urlEncode(new TextEncoder().encode(JSON.stringify(value)));
|
|
150
|
+
}
|
|
151
|
+
function decodeJson(s) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(new TextDecoder().decode(b64urlDecode(s)));
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function newRequestId() {
|
|
159
|
+
const bytes = new Uint8Array(16);
|
|
160
|
+
crypto.getRandomValues(bytes);
|
|
161
|
+
return b64urlEncode(bytes);
|
|
162
|
+
}
|
|
163
|
+
function openConsentWindow(apiUrl) {
|
|
164
|
+
const w = 440;
|
|
165
|
+
const h = 660;
|
|
166
|
+
const left = Math.max(0, (window.screenX ?? 0) + (window.outerWidth - w) / 2);
|
|
167
|
+
const top = Math.max(0, (window.screenY ?? 0) + (window.outerHeight - h) / 3);
|
|
168
|
+
try {
|
|
169
|
+
return window.open(
|
|
170
|
+
`${apiUrl}/authorize`,
|
|
171
|
+
"wordkey-authorize",
|
|
172
|
+
`popup=yes,width=${w},height=${h},left=${left},top=${top}`
|
|
173
|
+
);
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function runPopupFlow(input2) {
|
|
179
|
+
const { popup, apiOrigin, requestId, signal } = input2;
|
|
180
|
+
return new Promise((resolve) => {
|
|
181
|
+
let settled = false;
|
|
182
|
+
const cleanup = () => {
|
|
183
|
+
window.removeEventListener("message", onMsg);
|
|
184
|
+
clearInterval(closedPoll);
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
signal?.removeEventListener("abort", onAbort);
|
|
187
|
+
};
|
|
188
|
+
const finish = (r) => {
|
|
189
|
+
if (settled) return;
|
|
190
|
+
settled = true;
|
|
191
|
+
cleanup();
|
|
192
|
+
resolve(r);
|
|
193
|
+
};
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
try {
|
|
196
|
+
popup.close();
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
finish({ granted: false, values: {}, cancelled: true });
|
|
200
|
+
};
|
|
201
|
+
const onMsg = (e) => {
|
|
202
|
+
if (e.origin !== apiOrigin || e.source !== popup) return;
|
|
203
|
+
const data = e.data;
|
|
204
|
+
if (!data || typeof data !== "object") return;
|
|
205
|
+
if (data.type === "wordkey:authorize:ready") {
|
|
206
|
+
popup.postMessage(
|
|
207
|
+
{
|
|
208
|
+
type: "wordkey:authorize:init",
|
|
209
|
+
requestId,
|
|
210
|
+
publishableKey: input2.publishableKey,
|
|
211
|
+
ticket: input2.ticket,
|
|
212
|
+
fields: input2.fields
|
|
213
|
+
},
|
|
214
|
+
apiOrigin
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
if (data.type === "wordkey:profile:result" && data.requestId === requestId) {
|
|
218
|
+
finish({
|
|
219
|
+
granted: data.granted === true,
|
|
220
|
+
values: data.values ?? {}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
window.addEventListener("message", onMsg);
|
|
225
|
+
const closedPoll = setInterval(() => {
|
|
226
|
+
if (popup.closed) finish({ granted: false, values: {}, closed: true });
|
|
227
|
+
}, 400);
|
|
228
|
+
const timer = setTimeout(
|
|
229
|
+
() => finish({ granted: false, values: {} }),
|
|
230
|
+
POPUP_TIMEOUT_MS
|
|
231
|
+
);
|
|
232
|
+
if (signal) {
|
|
233
|
+
if (signal.aborted) onAbort();
|
|
234
|
+
else signal.addEventListener("abort", onAbort);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async function beginRedirectShare(input2) {
|
|
239
|
+
const pair = await crypto.subtle.generateKey(
|
|
240
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
241
|
+
true,
|
|
242
|
+
["deriveKey"]
|
|
243
|
+
);
|
|
244
|
+
const publicJwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
245
|
+
const privateKeyJwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
246
|
+
const pending = {
|
|
247
|
+
requestId: input2.requestId,
|
|
248
|
+
userId: input2.userId,
|
|
249
|
+
isNewUser: input2.isNewUser,
|
|
250
|
+
fields: input2.fields,
|
|
251
|
+
privateKeyJwk
|
|
252
|
+
};
|
|
253
|
+
sessionStorage.setItem(PENDING_STORE, JSON.stringify(pending));
|
|
254
|
+
const returnUrl = window.location.href.split("#")[0];
|
|
255
|
+
const request = {
|
|
256
|
+
v: 1,
|
|
257
|
+
requestId: input2.requestId,
|
|
258
|
+
publishableKey: input2.publishableKey,
|
|
259
|
+
ticket: input2.ticket,
|
|
260
|
+
fields: input2.fields,
|
|
261
|
+
origin: window.location.origin,
|
|
262
|
+
returnUrl,
|
|
263
|
+
sdkPub: publicJwk
|
|
264
|
+
};
|
|
265
|
+
window.location.assign(`${input2.apiUrl}/authorize#wkreq=${encodeJson(request)}`);
|
|
266
|
+
}
|
|
267
|
+
async function consumePendingRedirect() {
|
|
268
|
+
if (typeof window === "undefined") return null;
|
|
269
|
+
const hash = window.location.hash;
|
|
270
|
+
if (!hash.startsWith(RESULT_HASH)) return null;
|
|
271
|
+
const stored = sessionStorage.getItem(PENDING_STORE);
|
|
272
|
+
const envelope = decodeJson(
|
|
273
|
+
hash.slice(RESULT_HASH.length)
|
|
274
|
+
);
|
|
275
|
+
history.replaceState(null, "", window.location.pathname + window.location.search);
|
|
276
|
+
if (!stored || !envelope) return null;
|
|
277
|
+
let pending;
|
|
278
|
+
try {
|
|
279
|
+
pending = JSON.parse(stored);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
if (envelope.requestId !== pending.requestId) return null;
|
|
284
|
+
sessionStorage.removeItem(PENDING_STORE);
|
|
285
|
+
if (envelope.granted !== true) {
|
|
286
|
+
return { pending, result: { granted: false, values: {} } };
|
|
287
|
+
}
|
|
288
|
+
if (!envelope.epk || !envelope.iv || !envelope.ct) return null;
|
|
289
|
+
try {
|
|
290
|
+
const privateKey = await crypto.subtle.importKey(
|
|
291
|
+
"jwk",
|
|
292
|
+
pending.privateKeyJwk,
|
|
293
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
294
|
+
false,
|
|
295
|
+
["deriveKey"]
|
|
296
|
+
);
|
|
297
|
+
const peer = await crypto.subtle.importKey(
|
|
298
|
+
"jwk",
|
|
299
|
+
envelope.epk,
|
|
300
|
+
{ name: "ECDH", namedCurve: "P-256" },
|
|
301
|
+
false,
|
|
302
|
+
[]
|
|
303
|
+
);
|
|
304
|
+
const key = await crypto.subtle.deriveKey(
|
|
305
|
+
{ name: "ECDH", public: peer },
|
|
306
|
+
privateKey,
|
|
307
|
+
{ name: "AES-GCM", length: 256 },
|
|
308
|
+
false,
|
|
309
|
+
["decrypt"]
|
|
310
|
+
);
|
|
311
|
+
const plain = await crypto.subtle.decrypt(
|
|
312
|
+
{ name: "AES-GCM", iv: b64urlDecode(envelope.iv) },
|
|
313
|
+
key,
|
|
314
|
+
b64urlDecode(envelope.ct)
|
|
315
|
+
);
|
|
316
|
+
const parsed = JSON.parse(new TextDecoder().decode(plain));
|
|
317
|
+
return {
|
|
318
|
+
pending,
|
|
319
|
+
result: { granted: true, values: parsed.values ?? {} }
|
|
320
|
+
};
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/fonts.ts
|
|
327
|
+
var loaded = false;
|
|
328
|
+
function ensureFont() {
|
|
329
|
+
if (loaded || typeof document === "undefined") return;
|
|
330
|
+
loaded = true;
|
|
331
|
+
const href = "https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600&display=swap";
|
|
332
|
+
const link = document.createElement("link");
|
|
333
|
+
link.rel = "stylesheet";
|
|
334
|
+
link.href = href;
|
|
335
|
+
link.media = "print";
|
|
336
|
+
link.onload = () => {
|
|
337
|
+
link.media = "all";
|
|
338
|
+
};
|
|
339
|
+
document.head.appendChild(link);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/theme.ts
|
|
343
|
+
var C = {
|
|
344
|
+
navy: "#0B1529",
|
|
345
|
+
navy2: "#111E38",
|
|
346
|
+
navy3: "#1A2B50",
|
|
347
|
+
purple: "#7C3AED",
|
|
348
|
+
purpleL: "#A78BFA",
|
|
349
|
+
purpleD: "#5B21B6",
|
|
350
|
+
cream: "#F4F4F0",
|
|
351
|
+
muted: "#7A8BAD",
|
|
352
|
+
teal: "#0D9488",
|
|
353
|
+
red: "#EF4444",
|
|
354
|
+
border: "rgba(124,58,237,0.2)"
|
|
355
|
+
};
|
|
356
|
+
var FONT_DISPLAY = "'Cormorant Garamond', Georgia, 'Times New Roman', serif";
|
|
357
|
+
var FONT_BODY = "'DM Sans', system-ui, -apple-system, Segoe UI, Roboto, sans-serif";
|
|
358
|
+
var overlay = {
|
|
359
|
+
position: "fixed",
|
|
360
|
+
inset: 0,
|
|
361
|
+
zIndex: 2147483e3,
|
|
362
|
+
display: "flex",
|
|
363
|
+
alignItems: "center",
|
|
364
|
+
justifyContent: "center",
|
|
365
|
+
background: "rgba(0,0,0,0.7)",
|
|
366
|
+
backdropFilter: "blur(4px)",
|
|
367
|
+
padding: 16,
|
|
368
|
+
fontFamily: FONT_BODY
|
|
369
|
+
};
|
|
370
|
+
var panel = {
|
|
371
|
+
width: "100%",
|
|
372
|
+
maxWidth: 420,
|
|
373
|
+
background: C.navy2,
|
|
374
|
+
border: `1px solid ${C.border}`,
|
|
375
|
+
borderRadius: 20,
|
|
376
|
+
boxShadow: "0 20px 60px rgba(0,0,0,0.5)",
|
|
377
|
+
padding: 28,
|
|
378
|
+
color: C.cream,
|
|
379
|
+
boxSizing: "border-box",
|
|
380
|
+
maxHeight: "90vh",
|
|
381
|
+
overflowY: "auto"
|
|
382
|
+
};
|
|
383
|
+
var input = {
|
|
384
|
+
width: "100%",
|
|
385
|
+
height: 46,
|
|
386
|
+
borderRadius: 12,
|
|
387
|
+
border: `1px solid ${C.border}`,
|
|
388
|
+
background: C.navy,
|
|
389
|
+
color: C.cream,
|
|
390
|
+
padding: "0 14px",
|
|
391
|
+
fontSize: 15,
|
|
392
|
+
fontFamily: FONT_BODY,
|
|
393
|
+
boxSizing: "border-box",
|
|
394
|
+
outline: "none"
|
|
395
|
+
};
|
|
396
|
+
function primaryBtn(disabled) {
|
|
397
|
+
return {
|
|
398
|
+
flex: 1,
|
|
399
|
+
height: 46,
|
|
400
|
+
borderRadius: 12,
|
|
401
|
+
border: "none",
|
|
402
|
+
background: disabled ? "#3a2d63" : C.purple,
|
|
403
|
+
color: C.cream,
|
|
404
|
+
fontSize: 15,
|
|
405
|
+
fontWeight: 600,
|
|
406
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
407
|
+
fontFamily: FONT_BODY
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
var ghostBtn = {
|
|
411
|
+
flex: 1,
|
|
412
|
+
height: 46,
|
|
413
|
+
borderRadius: 12,
|
|
414
|
+
border: `1px solid ${C.border}`,
|
|
415
|
+
background: "transparent",
|
|
416
|
+
color: C.cream,
|
|
417
|
+
fontSize: 15,
|
|
418
|
+
cursor: "pointer",
|
|
419
|
+
fontFamily: FONT_BODY
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
// src/WordKeyButton.tsx
|
|
423
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
424
|
+
var sizePx = { sm: 20, md: 28, lg: 38 };
|
|
425
|
+
function WordKeyButton({
|
|
426
|
+
publishableKey,
|
|
427
|
+
onSuccess,
|
|
428
|
+
onError,
|
|
429
|
+
size = "md",
|
|
430
|
+
label = "Word?",
|
|
431
|
+
requestedFields,
|
|
432
|
+
apiUrl = DEFAULT_API_URL
|
|
433
|
+
}) {
|
|
434
|
+
const [open, setOpen] = (0, import_react.useState)(false);
|
|
435
|
+
(0, import_react.useEffect)(() => {
|
|
436
|
+
ensureFont();
|
|
437
|
+
}, []);
|
|
438
|
+
const resumed = (0, import_react.useRef)(false);
|
|
439
|
+
(0, import_react.useEffect)(() => {
|
|
440
|
+
if (resumed.current) return;
|
|
441
|
+
resumed.current = true;
|
|
442
|
+
void consumePendingRedirect().then((r) => {
|
|
443
|
+
if (!r) return;
|
|
444
|
+
const profile = buildProfile(
|
|
445
|
+
r.pending.fields,
|
|
446
|
+
r.result.granted ? r.result.values : {}
|
|
447
|
+
);
|
|
448
|
+
onSuccess({
|
|
449
|
+
id: r.pending.userId,
|
|
450
|
+
isNewUser: r.pending.isNewUser,
|
|
451
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
452
|
+
profile
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}, []);
|
|
456
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
457
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
458
|
+
"button",
|
|
459
|
+
{
|
|
460
|
+
onClick: () => setOpen(true),
|
|
461
|
+
style: {
|
|
462
|
+
background: "transparent",
|
|
463
|
+
border: "none",
|
|
464
|
+
padding: 0,
|
|
465
|
+
cursor: "pointer",
|
|
466
|
+
fontFamily: FONT_DISPLAY,
|
|
467
|
+
fontWeight: 600,
|
|
468
|
+
fontSize: sizePx[size],
|
|
469
|
+
lineHeight: 1
|
|
470
|
+
},
|
|
471
|
+
"aria-label": label,
|
|
472
|
+
children: [
|
|
473
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: C.purple }, children: "Word" }),
|
|
474
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: C.purpleL }, children: "?" })
|
|
475
|
+
]
|
|
476
|
+
}
|
|
477
|
+
),
|
|
478
|
+
open && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
479
|
+
AuthModal,
|
|
480
|
+
{
|
|
481
|
+
publishableKey,
|
|
482
|
+
apiUrl,
|
|
483
|
+
requestedFields,
|
|
484
|
+
onClose: () => setOpen(false),
|
|
485
|
+
onSuccess: (u) => {
|
|
486
|
+
setOpen(false);
|
|
487
|
+
onSuccess(u);
|
|
488
|
+
},
|
|
489
|
+
onError: (e) => onError?.(e)
|
|
490
|
+
}
|
|
491
|
+
)
|
|
492
|
+
] });
|
|
493
|
+
}
|
|
494
|
+
function AuthModal({
|
|
495
|
+
publishableKey,
|
|
496
|
+
apiUrl,
|
|
497
|
+
requestedFields,
|
|
498
|
+
onClose,
|
|
499
|
+
onSuccess,
|
|
500
|
+
onError
|
|
501
|
+
}) {
|
|
502
|
+
const fields = (0, import_react.useMemo)(
|
|
503
|
+
() => requestedFields ? resolveFields(requestedFields) : [],
|
|
504
|
+
[requestedFields]
|
|
505
|
+
);
|
|
506
|
+
const apiOrigin = (0, import_react.useMemo)(() => {
|
|
507
|
+
try {
|
|
508
|
+
return new URL(apiUrl).origin;
|
|
509
|
+
} catch {
|
|
510
|
+
return apiUrl;
|
|
511
|
+
}
|
|
512
|
+
}, [apiUrl]);
|
|
513
|
+
const [step, setStep] = (0, import_react.useState)("word");
|
|
514
|
+
const [word, setWord] = (0, import_react.useState)("");
|
|
515
|
+
const [error, setError] = (0, import_react.useState)(null);
|
|
516
|
+
const abortRef = (0, import_react.useRef)(null);
|
|
517
|
+
if (!abortRef.current) abortRef.current = new AbortController();
|
|
518
|
+
(0, import_react.useEffect)(() => () => abortRef.current?.abort(), []);
|
|
519
|
+
const [approvalQr, setApprovalQr] = (0, import_react.useState)(null);
|
|
520
|
+
const [approvalUrl, setApprovalUrl] = (0, import_react.useState)(null);
|
|
521
|
+
const pendingApproval = (0, import_react.useRef)(null);
|
|
522
|
+
async function authenticate() {
|
|
523
|
+
setError(null);
|
|
524
|
+
setStep("authenticating");
|
|
525
|
+
try {
|
|
526
|
+
const result = await verifyWord(apiUrl, publishableKey, word);
|
|
527
|
+
await completeAuth(result.userId, result.isNewUser, result.profileTicket);
|
|
528
|
+
} catch (e) {
|
|
529
|
+
if (e instanceof DeviceApprovalRequiredError) {
|
|
530
|
+
await startDeviceApproval();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const message = e.message;
|
|
534
|
+
setError(message);
|
|
535
|
+
setStep("error");
|
|
536
|
+
onError({ code: "auth_failed", message });
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async function completeAuth(userId, isNewUser, profileTicket) {
|
|
540
|
+
const finish = (profile) => onSuccess({
|
|
541
|
+
id: userId,
|
|
542
|
+
isNewUser,
|
|
543
|
+
authenticatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
544
|
+
profile
|
|
545
|
+
});
|
|
546
|
+
if (fields.length === 0) {
|
|
547
|
+
finish();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const requestId = newRequestId();
|
|
551
|
+
const popup = openConsentWindow(apiUrl);
|
|
552
|
+
if (!popup) {
|
|
553
|
+
await beginRedirectShare({
|
|
554
|
+
apiUrl,
|
|
555
|
+
requestId,
|
|
556
|
+
publishableKey,
|
|
557
|
+
ticket: profileTicket,
|
|
558
|
+
fields,
|
|
559
|
+
userId,
|
|
560
|
+
isNewUser
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
setStep("sharing");
|
|
565
|
+
const result = await runPopupFlow({
|
|
566
|
+
popup,
|
|
567
|
+
apiOrigin,
|
|
568
|
+
requestId,
|
|
569
|
+
publishableKey,
|
|
570
|
+
ticket: profileTicket,
|
|
571
|
+
fields,
|
|
572
|
+
signal: abortRef.current?.signal
|
|
573
|
+
});
|
|
574
|
+
if (result.cancelled) return;
|
|
575
|
+
finish(buildProfile(fields, result.granted ? result.values : {}));
|
|
576
|
+
}
|
|
577
|
+
async function startDeviceApproval() {
|
|
578
|
+
setStep("device-approval");
|
|
579
|
+
setError(null);
|
|
580
|
+
try {
|
|
581
|
+
const holderSecret = generateHolderSecret();
|
|
582
|
+
const res = await fetch(`${apiUrl}/api/auth/device-approval/create`, {
|
|
583
|
+
method: "POST",
|
|
584
|
+
headers: { "Content-Type": "application/json" },
|
|
585
|
+
body: JSON.stringify({
|
|
586
|
+
publishableKey,
|
|
587
|
+
deviceId: getDeviceId(),
|
|
588
|
+
holderSecret
|
|
589
|
+
})
|
|
590
|
+
});
|
|
591
|
+
const data = await res.json();
|
|
592
|
+
if (!res.ok) {
|
|
593
|
+
throw new Error(data?.error?.message ?? "Could not start phone approval");
|
|
594
|
+
}
|
|
595
|
+
setApprovalUrl(data.approveUrl);
|
|
596
|
+
try {
|
|
597
|
+
const QRCode = await import("qrcode");
|
|
598
|
+
setApprovalQr(
|
|
599
|
+
await QRCode.toDataURL(data.approveUrl, {
|
|
600
|
+
margin: 1,
|
|
601
|
+
color: { dark: "#A78BFA", light: "#0B1529" }
|
|
602
|
+
})
|
|
603
|
+
);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
void pollDeviceApproval(data.sessionKey, holderSecret);
|
|
607
|
+
} catch (e) {
|
|
608
|
+
const message = e.message;
|
|
609
|
+
setError(message);
|
|
610
|
+
setStep("error");
|
|
611
|
+
onError({ code: "device_approval_failed", message });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function pollDeviceApproval(sessionKey, holderSecret) {
|
|
615
|
+
for (let i = 0; i < 80; i++) {
|
|
616
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
617
|
+
let data;
|
|
618
|
+
try {
|
|
619
|
+
const res = await fetch(
|
|
620
|
+
`${apiUrl}/api/auth/device-approval/status/${sessionKey}`,
|
|
621
|
+
{ headers: { "X-WordKey-Approval-Secret": holderSecret } }
|
|
622
|
+
);
|
|
623
|
+
data = await res.json();
|
|
624
|
+
} catch {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (data.status === "approved" && data.token) {
|
|
628
|
+
const claims = decodeToken(data.token);
|
|
629
|
+
const userId = claims?.userId ?? "";
|
|
630
|
+
const isNewUser = claims?.isNewUser ?? false;
|
|
631
|
+
if (fields.length === 0) {
|
|
632
|
+
await completeAuth(userId, isNewUser, data.profileTicket);
|
|
633
|
+
} else {
|
|
634
|
+
pendingApproval.current = {
|
|
635
|
+
userId,
|
|
636
|
+
isNewUser,
|
|
637
|
+
profileTicket: data.profileTicket
|
|
638
|
+
};
|
|
639
|
+
setStep("approved");
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (data.status === "expired") {
|
|
644
|
+
setError("The approval request expired. Try again.");
|
|
645
|
+
setStep("error");
|
|
646
|
+
onError({
|
|
647
|
+
code: "device_approval_expired",
|
|
648
|
+
message: "Approval request expired"
|
|
649
|
+
});
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
setError("Approval timed out. Try again.");
|
|
654
|
+
setStep("error");
|
|
655
|
+
onError({ code: "device_approval_timeout", message: "Approval timed out" });
|
|
656
|
+
}
|
|
657
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: overlay, role: "dialog", "aria-modal": "true", onClick: onClose, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: panel, onClick: (e) => e.stopPropagation(), children: [
|
|
658
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Header, {}),
|
|
659
|
+
step === "word" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
660
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted, marginTop: 0, fontSize: 14 }, children: "Enter your word to continue." }),
|
|
661
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
662
|
+
WordInput,
|
|
663
|
+
{
|
|
664
|
+
autoFocus: true,
|
|
665
|
+
value: word,
|
|
666
|
+
onChange: (e) => setWord(e.target.value),
|
|
667
|
+
onKeyDown: (e) => {
|
|
668
|
+
if (e.key === "Enter" && word.trim().length >= 3)
|
|
669
|
+
void authenticate();
|
|
670
|
+
},
|
|
671
|
+
placeholder: "Your word",
|
|
672
|
+
style: { ...input, textTransform: "lowercase" }
|
|
673
|
+
}
|
|
674
|
+
),
|
|
675
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorText, { children: error }),
|
|
676
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Row, { children: [
|
|
677
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }),
|
|
678
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
679
|
+
"button",
|
|
680
|
+
{
|
|
681
|
+
style: primaryBtn(word.trim().length < 3),
|
|
682
|
+
disabled: word.trim().length < 3,
|
|
683
|
+
onClick: () => void authenticate(),
|
|
684
|
+
children: "Continue"
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
] }),
|
|
688
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Footer, {})
|
|
689
|
+
] }),
|
|
690
|
+
step === "authenticating" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "24px 0" }, children: [
|
|
691
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
|
|
692
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted }, children: "Verifying\u2026" })
|
|
693
|
+
] }),
|
|
694
|
+
step === "sharing" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "12px 0" }, children: [
|
|
695
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}),
|
|
696
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 22, margin: "4px 0" }, children: "Finish in the WordKey window" }),
|
|
697
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted, fontSize: 13 }, children: "Review what this site will receive in the window that just opened. Closing it signs you in without sharing your details." })
|
|
698
|
+
] }),
|
|
699
|
+
step === "device-approval" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center" }, children: [
|
|
700
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 24, margin: "4px 0" }, children: "Approve on your phone" }),
|
|
701
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted, fontSize: 14, marginTop: 4 }, children: "This device isn't recognized. Scan with the phone where your word lives, then approve." }),
|
|
702
|
+
approvalQr ? (
|
|
703
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
704
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
705
|
+
"img",
|
|
706
|
+
{
|
|
707
|
+
src: approvalQr,
|
|
708
|
+
alt: "Scan to approve on your phone",
|
|
709
|
+
width: 180,
|
|
710
|
+
height: 180,
|
|
711
|
+
style: { margin: "16px auto", display: "block", borderRadius: 12 }
|
|
712
|
+
}
|
|
713
|
+
)
|
|
714
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "24px 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, {}) }),
|
|
715
|
+
approvalUrl && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
716
|
+
"a",
|
|
717
|
+
{
|
|
718
|
+
href: approvalUrl,
|
|
719
|
+
target: "_blank",
|
|
720
|
+
rel: "noopener noreferrer",
|
|
721
|
+
style: { color: C.purpleL, fontSize: 12, wordBreak: "break-all" },
|
|
722
|
+
children: "Or open the approval link"
|
|
723
|
+
}
|
|
724
|
+
),
|
|
725
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted, fontSize: 12, marginTop: 12 }, children: "Waiting for approval\u2026" }),
|
|
726
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorText, { children: error }),
|
|
727
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Row, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }) })
|
|
728
|
+
] }),
|
|
729
|
+
step === "approved" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { textAlign: "center", padding: "12px 0" }, children: [
|
|
730
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 34, color: C.teal }, children: "\u2713" }),
|
|
731
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { style: { fontFamily: FONT_DISPLAY, fontSize: 24, margin: "8px 0 4px" }, children: "Approved on your phone" }),
|
|
732
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.muted, fontSize: 14 }, children: "Continue to review what this site will receive." }),
|
|
733
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Row, { children: [
|
|
734
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: ghostBtn, onClick: onClose, children: "Cancel" }),
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
736
|
+
"button",
|
|
737
|
+
{
|
|
738
|
+
style: primaryBtn(false),
|
|
739
|
+
onClick: () => {
|
|
740
|
+
const p = pendingApproval.current;
|
|
741
|
+
if (p) void completeAuth(p.userId, p.isNewUser, p.profileTicket);
|
|
742
|
+
},
|
|
743
|
+
children: "Continue"
|
|
744
|
+
}
|
|
745
|
+
)
|
|
746
|
+
] })
|
|
747
|
+
] }),
|
|
748
|
+
step === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
749
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ErrorText, { children: error }),
|
|
750
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Row, { children: [
|
|
751
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { style: ghostBtn, onClick: onClose, children: "Close" }),
|
|
752
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
753
|
+
"button",
|
|
754
|
+
{
|
|
755
|
+
style: primaryBtn(false),
|
|
756
|
+
onClick: () => {
|
|
757
|
+
setError(null);
|
|
758
|
+
setStep("word");
|
|
759
|
+
},
|
|
760
|
+
children: "Try again"
|
|
761
|
+
}
|
|
762
|
+
)
|
|
763
|
+
] })
|
|
764
|
+
] })
|
|
765
|
+
] }) });
|
|
766
|
+
}
|
|
767
|
+
function WordInput({
|
|
768
|
+
style,
|
|
769
|
+
...props
|
|
770
|
+
}) {
|
|
771
|
+
const [masked, setMasked] = (0, import_react.useState)(false);
|
|
772
|
+
(0, import_react.useEffect)(() => {
|
|
773
|
+
try {
|
|
774
|
+
if (typeof CSS !== "undefined" && CSS.supports("-webkit-text-security", "disc")) {
|
|
775
|
+
setMasked(true);
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
}, []);
|
|
780
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
781
|
+
"input",
|
|
782
|
+
{
|
|
783
|
+
type: masked ? "text" : "password",
|
|
784
|
+
name: "word",
|
|
785
|
+
autoComplete: "off",
|
|
786
|
+
autoCapitalize: "none",
|
|
787
|
+
autoCorrect: "off",
|
|
788
|
+
spellCheck: false,
|
|
789
|
+
inputMode: "text",
|
|
790
|
+
style: masked ? { ...style, WebkitTextSecurity: "disc" } : style,
|
|
791
|
+
...props
|
|
792
|
+
}
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
function valuePresent(v) {
|
|
796
|
+
if (v === void 0) return false;
|
|
797
|
+
if (typeof v === "string") return v.trim().length > 0;
|
|
798
|
+
return Object.values(v).some((x) => x && String(x).trim().length > 0);
|
|
799
|
+
}
|
|
800
|
+
function decodeToken(token) {
|
|
801
|
+
try {
|
|
802
|
+
const part = token.split(".")[1];
|
|
803
|
+
if (!part) return null;
|
|
804
|
+
const b64 = part.replace(/-/g, "+").replace(/_/g, "/");
|
|
805
|
+
const json = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("utf8");
|
|
806
|
+
return JSON.parse(json);
|
|
807
|
+
} catch {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
function buildProfile(fields, granted) {
|
|
812
|
+
const profile = {};
|
|
813
|
+
const custom = {};
|
|
814
|
+
for (const f of fields) {
|
|
815
|
+
const v = granted[f.key];
|
|
816
|
+
if (!valuePresent(v)) continue;
|
|
817
|
+
if (f.key === "name" && typeof v === "string") profile.name = v;
|
|
818
|
+
else if (f.key === "email" && typeof v === "string") profile.email = v;
|
|
819
|
+
else if (f.key === "phone" && typeof v === "string") profile.phone = v;
|
|
820
|
+
else if (f.key === "shipping_address" && typeof v === "object")
|
|
821
|
+
profile.shippingAddress = v;
|
|
822
|
+
else if (typeof v === "string") custom[f.key] = v;
|
|
823
|
+
}
|
|
824
|
+
if (Object.keys(custom).length) profile.custom = custom;
|
|
825
|
+
return profile;
|
|
826
|
+
}
|
|
827
|
+
function Header() {
|
|
828
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginBottom: 14 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontFamily: FONT_DISPLAY, fontSize: 28, fontWeight: 600 }, children: [
|
|
829
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: C.purple }, children: "Word" }),
|
|
830
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: C.purpleL }, children: "?" })
|
|
831
|
+
] }) });
|
|
832
|
+
}
|
|
833
|
+
function Footer() {
|
|
834
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { marginTop: 16, fontSize: 11, color: C.muted, textAlign: "center" }, children: "Secured by WordKey \xB7 one word, every door" });
|
|
835
|
+
}
|
|
836
|
+
function Row({ children }) {
|
|
837
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: 10, marginTop: 20 }, children });
|
|
838
|
+
}
|
|
839
|
+
function ErrorText({ children }) {
|
|
840
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: C.red, fontSize: 13, marginTop: 12 }, children });
|
|
841
|
+
}
|
|
842
|
+
function Spinner() {
|
|
843
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
844
|
+
"div",
|
|
845
|
+
{
|
|
846
|
+
style: {
|
|
847
|
+
width: 32,
|
|
848
|
+
height: 32,
|
|
849
|
+
margin: "0 auto 12px",
|
|
850
|
+
border: `3px solid ${C.border}`,
|
|
851
|
+
borderTopColor: C.purpleL,
|
|
852
|
+
borderRadius: "50%",
|
|
853
|
+
animation: "wk-spin 0.8s linear infinite"
|
|
854
|
+
},
|
|
855
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: "@keyframes wk-spin{to{transform:rotate(360deg)}}" })
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
860
|
+
0 && (module.exports = {
|
|
861
|
+
WordKeyButton
|
|
862
|
+
});
|