@seams/wallet 0.11.0 → 0.13.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 +3 -3
- package/dist/.build-inputs.sha256 +1 -1
- package/dist/esm/SeamsWeb/walletIframe/host/ui/auth-menu/AuthMenuSurface.js +252 -149
- package/dist/esm/SeamsWeb/walletIframe/host/ui/auth-menu/linkDeviceBadge.js +90 -0
- package/dist/esm/sdk/{host-Bqmmy8Ts.js → host-GawN20iA.js} +1 -1
- package/dist/{public/sdk/pendingWalletRecoveryCodeBackup-CMCOUuA-.js → esm/sdk/pendingWalletRecoveryCodeBackup-DMg5U0Fk.js} +1 -1
- package/dist/{public/sdk/recovery-entrypoint-QxTiyx-o.js → esm/sdk/recovery-entrypoint-EHY_b7Z-.js} +1 -1
- package/dist/{public/sdk/registration-eyzHqLHi.js → esm/sdk/registration-CQxxUt5s.js} +1 -1
- package/dist/esm/sdk/runtime-auth-TZw6JD8N.js +1 -0
- package/dist/esm/sdk/{runtime-device-link-CHsqrEQ9.js → runtime-device-link-DXjabXJb.js} +1 -1
- package/dist/esm/sdk/{runtime-ecdsa-tempo-MgmnU9g-.js → runtime-ecdsa-tempo-DlKMIbyP.js} +1 -1
- package/dist/esm/sdk/{runtime-email-otp-Cj96n102.js → runtime-email-otp-BBPghU2G.js} +1 -1
- package/dist/esm/sdk/{runtime-export-mcwZRxKk.js → runtime-export-pw-xJL8o.js} +1 -1
- package/dist/esm/sdk/{runtime-near-BMdTtu7D.js → runtime-near-B-vYxGjl.js} +1 -1
- package/dist/esm/sdk/{runtime-preferences-CBlhE8bX.js → runtime-preferences-D-hY0RfC.js} +1 -1
- package/dist/esm/sdk/{runtime-recovery-mJp3MLeG.js → runtime-recovery-Vg1ND2nl.js} +1 -1
- package/dist/esm/sdk/{runtime-recovery-codes-B1gixZsp.js → runtime-recovery-codes-rLR4Guy4.js} +1 -1
- package/dist/esm/sdk/{shared-8Qe7Nxgg.js → shared-6bpRPbcM.js} +1 -1
- package/dist/esm/sdk/wallet-iframe-host-ecdsa.js +1 -1
- package/dist/esm/sdk/wallet-iframe-host-full.js +1 -1
- package/dist/esm/sdk/wallet-iframe-host-near.js +1 -1
- package/dist/esm/sdk/wallet-iframe-host-runtime.js +1 -1
- package/dist/esm/sdk/{wallet-recovery-D7ywcIHw.js → wallet-recovery-LMtGiEL3.js} +1 -1
- package/dist/esm/sdk/wallet-ui.css +1 -1
- package/dist/esm/sdk/{walletCustodyEcdsaContinuity-_IKoCUBO.js → walletCustodyEcdsaContinuity-D-gYxv6e.js} +1 -1
- package/dist/esm/sdk/{walletRecoveryCommit-DDPa1rAJ.js → walletRecoveryCommit-BYRC_2li.js} +1 -1
- package/dist/public/headers.manifest.json +1 -1
- package/dist/public/sdk/{host-Bqmmy8Ts.js → host-GawN20iA.js} +1 -1
- package/dist/{esm/sdk/pendingWalletRecoveryCodeBackup-CMCOUuA-.js → public/sdk/pendingWalletRecoveryCodeBackup-DMg5U0Fk.js} +1 -1
- package/dist/{esm/sdk/recovery-entrypoint-QxTiyx-o.js → public/sdk/recovery-entrypoint-EHY_b7Z-.js} +1 -1
- package/dist/{esm/sdk/registration-eyzHqLHi.js → public/sdk/registration-CQxxUt5s.js} +1 -1
- package/dist/public/sdk/runtime-auth-TZw6JD8N.js +1 -0
- package/dist/public/sdk/{runtime-device-link-CHsqrEQ9.js → runtime-device-link-DXjabXJb.js} +1 -1
- package/dist/public/sdk/{runtime-ecdsa-tempo-MgmnU9g-.js → runtime-ecdsa-tempo-DlKMIbyP.js} +1 -1
- package/dist/public/sdk/{runtime-email-otp-Cj96n102.js → runtime-email-otp-BBPghU2G.js} +1 -1
- package/dist/public/sdk/{runtime-export-mcwZRxKk.js → runtime-export-pw-xJL8o.js} +1 -1
- package/dist/public/sdk/{runtime-near-BMdTtu7D.js → runtime-near-B-vYxGjl.js} +1 -1
- package/dist/public/sdk/{runtime-preferences-CBlhE8bX.js → runtime-preferences-D-hY0RfC.js} +1 -1
- package/dist/public/sdk/{runtime-recovery-mJp3MLeG.js → runtime-recovery-Vg1ND2nl.js} +1 -1
- package/dist/public/sdk/{runtime-recovery-codes-B1gixZsp.js → runtime-recovery-codes-rLR4Guy4.js} +1 -1
- package/dist/public/sdk/{shared-8Qe7Nxgg.js → shared-6bpRPbcM.js} +1 -1
- package/dist/public/sdk/wallet-iframe-host-ecdsa.js +1 -1
- package/dist/public/sdk/wallet-iframe-host-full.js +1 -1
- package/dist/public/sdk/wallet-iframe-host-near.js +1 -1
- package/dist/public/sdk/wallet-iframe-host-runtime.js +1 -1
- package/dist/public/sdk/{wallet-recovery-D7ywcIHw.js → wallet-recovery-LMtGiEL3.js} +1 -1
- package/dist/public/sdk/wallet-ui.css +1 -1
- package/dist/public/sdk/{walletCustodyEcdsaContinuity-_IKoCUBO.js → walletCustodyEcdsaContinuity-D-gYxv6e.js} +1 -1
- package/dist/public/sdk/{walletRecoveryCommit-DDPa1rAJ.js → walletRecoveryCommit-BYRC_2li.js} +1 -1
- package/dist/public/wallet-assets.manifest.json +38 -38
- package/dist/public/wallet-service/index.html +4 -4
- package/dist/types/wallet/src/SeamsWeb/walletIframe/host/auth-menu/domain.d.ts +7 -3
- package/dist/types/wallet/src/SeamsWeb/walletIframe/host/auth-menu/session.d.ts +3 -1
- package/dist/types/wallet/src/SeamsWeb/walletIframe/host/ui/auth-menu/AuthMenuSurface.d.ts +26 -5
- package/dist/types/wallet/src/SeamsWeb/walletIframe/host/ui/auth-menu/linkDeviceBadge.d.ts +18 -0
- package/package.json +1 -3
- package/dist/esm/sdk/runtime-auth-CbveTM2_.js +0 -1
- package/dist/public/sdk/runtime-auth-CbveTM2_.js +0 -1
package/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# Seams Wallet SDK
|
|
2
2
|
|
|
3
|
-
`@seams/wallet` **0.
|
|
3
|
+
`@seams/wallet` **0.13.0** provides browser wallet lifecycle, NEAR and EVM-family
|
|
4
4
|
threshold signing, React components, and hosted wallet runtime assets.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
pnpm add @seams/wallet@0.
|
|
9
|
+
pnpm add @seams/wallet@0.13.0
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
React integrations support React 18 and 19. Import the provider and UI from
|
|
13
13
|
`@seams/wallet/react` and its stylesheet from `@seams/wallet/react/styles`.
|
|
14
14
|
Keep the application package, wallet-origin assets, and
|
|
15
|
-
`@seams/wallet-server@0.
|
|
15
|
+
`@seams/wallet-server@0.13.0` on the matching release.
|
|
16
16
|
|
|
17
17
|
## Next release migration (unreleased)
|
|
18
18
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
264938a73999cf6e6b1da2cf1272943fb8258e8c3a5fee0fce16e21e0a64d0f3
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isLinkedDeviceTargetEmailAddressV1 } from "../../../../../core/types/linkDevice.js";
|
|
2
|
+
import { EMAIL_OTP_RECOVERY_KEY_ALPHABET } from "../../../../../shared-ts/src/utils/emailOtpRecoveryKey.js";
|
|
2
3
|
import "../../../../../shared-ts/src/wallet-recovery/recoveryCodes.js";
|
|
3
4
|
import { demoEmailOtpCodeFromDelivery } from "../../../../../core/signingEngine/session/emailOtp/challengeDelivery.js";
|
|
4
5
|
import { isAuthMenuActionReady, isAuthMenuGoogleActionReady, isAuthMenuLoadingStatus, isAuthMenuReady, resolveAuthMenuLoginAccount } from "../../auth-menu/domain.js";
|
|
@@ -6,6 +7,7 @@ import { PadlockIcon } from "../../../../../core/signingEngine/uiConfirm/ui/prea
|
|
|
6
7
|
import { blinkMenuItem } from "../../../../../utils/menuItemBlink.js";
|
|
7
8
|
import { accountDropdownIcon, backIcon, chevronIcon, fingerprintIcon, googleIcon, linkDeviceIcon, linkFailedIcon, mailIcon, recoveryIcon, rerollIcon } from "./icons.js";
|
|
8
9
|
import { AuthMenuFooter } from "./AuthMenuFooter.js";
|
|
10
|
+
import { linkDeviceDotRing, linkDeviceEmailOtpPresentation } from "./linkDeviceBadge.js";
|
|
9
11
|
import { Component, Fragment } from "preact";
|
|
10
12
|
import { Fragment as Fragment$1, jsx, jsxs } from "preact/jsx-runtime";
|
|
11
13
|
//#region src/SeamsWeb/walletIframe/host/ui/auth-menu/AuthMenuSurface.tsx
|
|
@@ -23,19 +25,74 @@ const AUTH_MENU_ACCOUNT_LIST_ID = "seams-auth-menu-account-list";
|
|
|
23
25
|
function otpCodeDigits(code, length = 6) {
|
|
24
26
|
return code.padEnd(length, " ").slice(0, length).split("");
|
|
25
27
|
}
|
|
28
|
+
const RECOVERY_CODE_CHARACTERS = new Set(EMAIL_OTP_RECOVERY_KEY_ALPHABET);
|
|
26
29
|
function recoveryInputCharacters(code) {
|
|
27
|
-
|
|
30
|
+
let characters = "";
|
|
31
|
+
for (const character of code.toUpperCase()) {
|
|
32
|
+
const read = character === "O" ? "0" : character === "I" || character === "L" ? "1" : character;
|
|
33
|
+
if (RECOVERY_CODE_CHARACTERS.has(read)) characters += read;
|
|
34
|
+
}
|
|
35
|
+
return characters;
|
|
36
|
+
}
|
|
37
|
+
const RECOVERY_CODE_SEPARATOR = /[\s\-\u2010-\u2015\u2212]/;
|
|
38
|
+
/** Characters the input dropped because no recovery code contains them, such as U. */
|
|
39
|
+
function rejectedRecoveryCharacters(input) {
|
|
40
|
+
const rejected = /* @__PURE__ */ new Set();
|
|
41
|
+
for (const character of input.toUpperCase()) {
|
|
42
|
+
if (RECOVERY_CODE_SEPARATOR.test(character)) continue;
|
|
43
|
+
if (recoveryInputCharacters(character)) continue;
|
|
44
|
+
rejected.add(character);
|
|
45
|
+
}
|
|
46
|
+
return Array.from(rejected).slice(0, 3);
|
|
28
47
|
}
|
|
29
|
-
function
|
|
48
|
+
function renderRecoveryCodeGroups(code, selection, arrival) {
|
|
49
|
+
const caret = selection && selection.start === selection.end ? selection.start : null;
|
|
50
|
+
const lastIndex = 31;
|
|
51
|
+
const focusIndex = selection === null ? null : Math.min(selection.start, lastIndex);
|
|
30
52
|
const groups = [];
|
|
31
|
-
for (let
|
|
53
|
+
for (let start = 0; start < 32; start += 4) {
|
|
54
|
+
const end = start + 4;
|
|
55
|
+
const slots = [];
|
|
56
|
+
for (let index = start; index < end; index += 1) {
|
|
57
|
+
const character = code[index] ?? "";
|
|
58
|
+
const classes = ["seams-otp-slot"];
|
|
59
|
+
if (character) classes.push("is-filled");
|
|
60
|
+
if (index === caret) classes.push("is-caret");
|
|
61
|
+
if (caret === 32 && index === lastIndex) classes.push("is-caret-after");
|
|
62
|
+
if (selection && index >= selection.start && index < selection.end) classes.push("is-selected");
|
|
63
|
+
if (arrival && index >= arrival.start && index < arrival.end) classes.push("is-new");
|
|
64
|
+
slots.push(/* @__PURE__ */ jsx("span", {
|
|
65
|
+
class: classes.join(" "),
|
|
66
|
+
"data-recovery-slot": index,
|
|
67
|
+
children: character
|
|
68
|
+
}, index));
|
|
69
|
+
}
|
|
70
|
+
const groupClasses = ["seams-recovery-code-group"];
|
|
71
|
+
if (focusIndex !== null && focusIndex >= start && focusIndex < end) groupClasses.push("is-active");
|
|
72
|
+
if (code.length >= end) groupClasses.push("is-complete");
|
|
73
|
+
groups.push(/* @__PURE__ */ jsx("div", {
|
|
74
|
+
class: groupClasses.join(" "),
|
|
75
|
+
children: slots
|
|
76
|
+
}, start));
|
|
77
|
+
}
|
|
32
78
|
return groups;
|
|
33
79
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
80
|
+
function recoveryCaretAtPoint(root, clientX, clientY, codeLength) {
|
|
81
|
+
let nearest = null;
|
|
82
|
+
for (const slot of root.querySelectorAll("[data-recovery-slot]")) {
|
|
83
|
+
const index = Number(slot.dataset.recoverySlot);
|
|
84
|
+
const box = slot.getBoundingClientRect();
|
|
85
|
+
const dx = clientX < box.left ? box.left - clientX : Math.max(0, clientX - box.right);
|
|
86
|
+
const dy = clientY < box.top ? box.top - clientY : Math.max(0, clientY - box.bottom);
|
|
87
|
+
const distance = dx * dx + dy * dy;
|
|
88
|
+
if (nearest === null || distance < nearest.distance) nearest = {
|
|
89
|
+
index,
|
|
90
|
+
distance,
|
|
91
|
+
after: clientX > box.left + box.width / 2
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (nearest === null) return null;
|
|
95
|
+
return Math.min(nearest.after ? nearest.index + 1 : nearest.index, codeLength);
|
|
39
96
|
}
|
|
40
97
|
function renderOtpCodeDigit(digit) {
|
|
41
98
|
return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx("span", {
|
|
@@ -54,6 +111,10 @@ function recoveryAnnouncement(viewModel) {
|
|
|
54
111
|
if (viewModel.stage === "sign_in_ready") return "Wallet recovered. Unlock it to continue.";
|
|
55
112
|
return "";
|
|
56
113
|
}
|
|
114
|
+
function recoveryBackLabel(viewModel) {
|
|
115
|
+
if (viewModel.kind !== "recovery") return "Back";
|
|
116
|
+
return viewModel.stage === "choose_method" ? "Back to recovery code" : "Back to unlock wallet";
|
|
117
|
+
}
|
|
57
118
|
function recoveryNavigationLocked(viewModel) {
|
|
58
119
|
return viewModel.kind === "recovery" && viewModel.stage === "finalizing";
|
|
59
120
|
}
|
|
@@ -79,90 +140,6 @@ function menuNotice(viewModel) {
|
|
|
79
140
|
if (viewModel.kind === "google_otp_login" || viewModel.kind === "google_registration") return viewModel.error || null;
|
|
80
141
|
return viewModel.kind === "passkey" && viewModel.status.kind === "recoverable" && viewModel.status.reason === "error" ? viewModel.status.message : null;
|
|
81
142
|
}
|
|
82
|
-
const LINK_DEVICE_DOT_COUNT = 12;
|
|
83
|
-
function linkDeviceDotRing(phase, glyph) {
|
|
84
|
-
return /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsxs("div", {
|
|
85
|
-
class: `seams-link-device-dot-ring is-${phase}`,
|
|
86
|
-
"aria-hidden": "true",
|
|
87
|
-
children: [
|
|
88
|
-
Array.from({ length: LINK_DEVICE_DOT_COUNT }, (_, index) => /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx("span", { "data-dot-index": index }) })),
|
|
89
|
-
glyph ? /* @__PURE__ */ jsx(Fragment$1, { children: /* @__PURE__ */ jsx("div", {
|
|
90
|
-
class: "seams-link-device-dot-ring-glyph",
|
|
91
|
-
children: glyph
|
|
92
|
-
}) }) : null,
|
|
93
|
-
/* @__PURE__ */ jsx("svg", {
|
|
94
|
-
class: "seams-link-device-dot-ring-check",
|
|
95
|
-
viewBox: "0 0 52 52",
|
|
96
|
-
fill: "none",
|
|
97
|
-
stroke: "currentColor",
|
|
98
|
-
"stroke-width": "2.5",
|
|
99
|
-
"stroke-linecap": "round",
|
|
100
|
-
"stroke-linejoin": "round",
|
|
101
|
-
children: /* @__PURE__ */ jsx("path", { d: "m18.5 27 5 5 10-11" })
|
|
102
|
-
})
|
|
103
|
-
]
|
|
104
|
-
}) });
|
|
105
|
-
}
|
|
106
|
-
function linkDeviceEmailOtpPresentation(activation) {
|
|
107
|
-
switch (activation.kind) {
|
|
108
|
-
case "sending": return {
|
|
109
|
-
ring: "waiting",
|
|
110
|
-
heading: "Sending your code",
|
|
111
|
-
status: "Emailing a 6-digit code",
|
|
112
|
-
pending: true,
|
|
113
|
-
tone: "neutral"
|
|
114
|
-
};
|
|
115
|
-
case "resending": return {
|
|
116
|
-
ring: "waiting",
|
|
117
|
-
heading: "Sending a new code",
|
|
118
|
-
status: "Emailing a fresh 6-digit code",
|
|
119
|
-
pending: true,
|
|
120
|
-
tone: "neutral"
|
|
121
|
-
};
|
|
122
|
-
case "submitting": return {
|
|
123
|
-
ring: "waiting",
|
|
124
|
-
heading: "Checking your code",
|
|
125
|
-
status: "Verifying the code you entered",
|
|
126
|
-
pending: true,
|
|
127
|
-
tone: "neutral"
|
|
128
|
-
};
|
|
129
|
-
case "code_input": return {
|
|
130
|
-
ring: "idle",
|
|
131
|
-
heading: "Verify your email",
|
|
132
|
-
status: "Enter the 6-digit code we just sent.",
|
|
133
|
-
pending: false,
|
|
134
|
-
tone: "neutral"
|
|
135
|
-
};
|
|
136
|
-
case "incorrect": return {
|
|
137
|
-
ring: "idle",
|
|
138
|
-
heading: "Verify your email",
|
|
139
|
-
status: activation.message,
|
|
140
|
-
pending: false,
|
|
141
|
-
tone: "error"
|
|
142
|
-
};
|
|
143
|
-
case "expired": return {
|
|
144
|
-
ring: "idle",
|
|
145
|
-
heading: "That code expired",
|
|
146
|
-
status: activation.message,
|
|
147
|
-
pending: false,
|
|
148
|
-
tone: "error"
|
|
149
|
-
};
|
|
150
|
-
case "unavailable": return {
|
|
151
|
-
ring: "idle",
|
|
152
|
-
heading: "Email code unavailable",
|
|
153
|
-
status: activation.message,
|
|
154
|
-
pending: false,
|
|
155
|
-
tone: "error"
|
|
156
|
-
};
|
|
157
|
-
case "completed": return {
|
|
158
|
-
ring: "approved",
|
|
159
|
-
heading: "Email verified",
|
|
160
|
-
status: "Finishing linking this device",
|
|
161
|
-
pending: true,
|
|
162
|
-
tone: "neutral"
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
143
|
function selectedLoginAccount(viewModel) {
|
|
167
144
|
return resolveAuthMenuLoginAccount(viewModel.accountOptions, viewModel.selectedAccount).selectedAccount;
|
|
168
145
|
}
|
|
@@ -194,7 +171,15 @@ const ACCOUNT_MENU_EXIT_MS = 160;
|
|
|
194
171
|
var AuthMenuSurface = class extends Component {
|
|
195
172
|
constructor(..._args) {
|
|
196
173
|
super(..._args);
|
|
197
|
-
this.state = {
|
|
174
|
+
this.state = {
|
|
175
|
+
accountMenu: "closed",
|
|
176
|
+
recoverySelection: null,
|
|
177
|
+
recoveryRejection: null
|
|
178
|
+
};
|
|
179
|
+
this.recoveryDrawnCode = "";
|
|
180
|
+
this.recoveryArrival = null;
|
|
181
|
+
this.recoveryArrivalTimer = null;
|
|
182
|
+
this.recoveryRejectionTimer = null;
|
|
198
183
|
this.accountMenuExitTimer = null;
|
|
199
184
|
this.accountMenuChoice = null;
|
|
200
185
|
this.previouslyFocusedElement = null;
|
|
@@ -367,24 +352,49 @@ var AuthMenuSurface = class extends Component {
|
|
|
367
352
|
};
|
|
368
353
|
this.onRecoveryCodeInput = (event) => {
|
|
369
354
|
if (!(event.currentTarget instanceof HTMLInputElement)) return;
|
|
370
|
-
|
|
371
|
-
if (viewModel.kind !== "recovery") return;
|
|
355
|
+
if (this.viewModel.kind !== "recovery") return;
|
|
372
356
|
const input = event.currentTarget;
|
|
373
|
-
const length = viewModel.recoveryCodeSlotCount ?? 32;
|
|
374
357
|
const raw = input.value;
|
|
358
|
+
const rejected = rejectedRecoveryCharacters(raw);
|
|
359
|
+
if (rejected.length > 0) this.showRecoveryRejection(rejected);
|
|
375
360
|
const caret = recoveryInputCharacters(raw.slice(0, input.selectionStart ?? raw.length)).length;
|
|
376
|
-
const code = recoveryInputCharacters(raw).slice(0,
|
|
361
|
+
const code = recoveryInputCharacters(raw).slice(0, 32);
|
|
377
362
|
if (input.value !== code) {
|
|
378
363
|
input.value = code;
|
|
379
364
|
input.setSelectionRange(Math.min(caret, code.length), Math.min(caret, code.length));
|
|
380
365
|
}
|
|
366
|
+
this.syncRecoverySelection(input);
|
|
381
367
|
this.emitIntent({
|
|
382
368
|
kind: "recovery_code_changed",
|
|
383
369
|
recoveryCode: code
|
|
384
370
|
});
|
|
385
371
|
};
|
|
372
|
+
this.onRecoverySelectionMove = (event) => {
|
|
373
|
+
if (event.currentTarget instanceof HTMLInputElement) this.syncRecoverySelection(event.currentTarget);
|
|
374
|
+
};
|
|
375
|
+
this.onRecoveryCodeClick = (event) => {
|
|
376
|
+
const input = event.currentTarget;
|
|
377
|
+
if (!(input instanceof HTMLInputElement)) return;
|
|
378
|
+
if (input.selectionStart === input.selectionEnd) {
|
|
379
|
+
const caret = recoveryCaretAtPoint(this.props.element, event.clientX, event.clientY, input.value.length);
|
|
380
|
+
if (caret !== null) input.setSelectionRange(caret, caret);
|
|
381
|
+
}
|
|
382
|
+
this.syncRecoverySelection(input);
|
|
383
|
+
};
|
|
384
|
+
this.clearRecoveryRejection = () => {
|
|
385
|
+
this.recoveryRejectionTimer = null;
|
|
386
|
+
this.setState({ recoveryRejection: null });
|
|
387
|
+
};
|
|
388
|
+
this.endRecoveryArrival = () => {
|
|
389
|
+
this.recoveryArrivalTimer = null;
|
|
390
|
+
this.recoveryArrival = null;
|
|
391
|
+
};
|
|
392
|
+
this.onRecoveryCodeBlur = () => {
|
|
393
|
+
this.setState({ recoverySelection: null });
|
|
394
|
+
};
|
|
386
395
|
this.onRecoverySubmit = (event) => {
|
|
387
396
|
event.preventDefault();
|
|
397
|
+
this.emitIntent({ kind: "recovery_code_submit" });
|
|
388
398
|
};
|
|
389
399
|
this.onRecoveryPasskeySelected = () => {
|
|
390
400
|
this.emitIntent({ kind: "recovery_passkey_selected" });
|
|
@@ -492,6 +502,8 @@ var AuthMenuSurface = class extends Component {
|
|
|
492
502
|
if (this.contentHeightFrame !== null) cancelAnimationFrame(this.contentHeightFrame);
|
|
493
503
|
this.contentHeightFrame = null;
|
|
494
504
|
this.endAccountMenuExit();
|
|
505
|
+
if (this.recoveryArrivalTimer !== null) clearTimeout(this.recoveryArrivalTimer);
|
|
506
|
+
if (this.recoveryRejectionTimer !== null) clearTimeout(this.recoveryRejectionTimer);
|
|
495
507
|
window.removeEventListener("resize", this.queueContentHeightSync);
|
|
496
508
|
document.removeEventListener("pointerdown", this.onDocumentPointerDown);
|
|
497
509
|
document.removeEventListener("keydown", this.onKeyDown, true);
|
|
@@ -524,6 +536,10 @@ var AuthMenuSurface = class extends Component {
|
|
|
524
536
|
if (previous?.kind !== "recovery" || previous.stage !== "enter_code") this.props.element.querySelector("[data-recovery-code]")?.focus();
|
|
525
537
|
return;
|
|
526
538
|
}
|
|
539
|
+
if (current.stage === "choose_method") {
|
|
540
|
+
if (stageChanged) this.props.element.querySelector("[data-recovery-target]")?.focus();
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
527
543
|
const becameRecoverable = current.status.kind === "recoverable" && previous?.kind === "recovery" && previous.status.kind !== "recoverable";
|
|
528
544
|
if ((stageChanged || becameRecoverable) && (current.stage === "passkey_ready" || current.stage === "finalizing" || current.stage === "sign_in_ready")) this.props.element.querySelector("[data-auth-menu-primary]")?.focus();
|
|
529
545
|
}
|
|
@@ -557,6 +573,41 @@ var AuthMenuSurface = class extends Component {
|
|
|
557
573
|
focusableElements() {
|
|
558
574
|
return Array.from(this.props.element.querySelectorAll("button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), a[href], [tabindex]:not([tabindex=\"-1\"])"));
|
|
559
575
|
}
|
|
576
|
+
showRecoveryRejection(characters) {
|
|
577
|
+
const parity = this.state.recoveryRejection?.parity === 0 ? 1 : 0;
|
|
578
|
+
this.setState({ recoveryRejection: {
|
|
579
|
+
characters,
|
|
580
|
+
parity
|
|
581
|
+
} });
|
|
582
|
+
if (this.recoveryRejectionTimer !== null) clearTimeout(this.recoveryRejectionTimer);
|
|
583
|
+
this.recoveryRejectionTimer = setTimeout(this.clearRecoveryRejection, 2e3);
|
|
584
|
+
}
|
|
585
|
+
recoveryCodeArrival(code) {
|
|
586
|
+
const drawn = this.recoveryDrawnCode;
|
|
587
|
+
if (code === drawn) return this.recoveryArrival;
|
|
588
|
+
this.recoveryDrawnCode = code;
|
|
589
|
+
let start = 0;
|
|
590
|
+
while (start < drawn.length && drawn[start] === code[start]) start += 1;
|
|
591
|
+
const end = start + code.length - drawn.length;
|
|
592
|
+
this.recoveryArrival = code.length > drawn.length ? {
|
|
593
|
+
start,
|
|
594
|
+
end
|
|
595
|
+
} : null;
|
|
596
|
+
if (this.recoveryArrivalTimer !== null) clearTimeout(this.recoveryArrivalTimer);
|
|
597
|
+
this.recoveryArrivalTimer = setTimeout(this.endRecoveryArrival, 600);
|
|
598
|
+
return this.recoveryArrival;
|
|
599
|
+
}
|
|
600
|
+
syncRecoverySelection(input) {
|
|
601
|
+
const length = input.value.length;
|
|
602
|
+
const start = Math.min(input.selectionStart ?? length, length);
|
|
603
|
+
const end = Math.min(Math.max(input.selectionEnd ?? start, start), length);
|
|
604
|
+
const current = this.state.recoverySelection;
|
|
605
|
+
if (current?.start === start && current.end === end) return;
|
|
606
|
+
this.setState({ recoverySelection: {
|
|
607
|
+
start,
|
|
608
|
+
end
|
|
609
|
+
} });
|
|
610
|
+
}
|
|
560
611
|
emitIntent(intent) {
|
|
561
612
|
this.props.onIntent(intent);
|
|
562
613
|
}
|
|
@@ -595,7 +646,7 @@ var AuthMenuSurface = class extends Component {
|
|
|
595
646
|
/* @__PURE__ */ jsx("button", {
|
|
596
647
|
class: `seams-back-button ${loading || linkDevice || otpPrompt || registrationPrompt || recovery ? "is-visible" : ""}`,
|
|
597
648
|
type: "button",
|
|
598
|
-
"aria-label":
|
|
649
|
+
"aria-label": recoveryBackLabel(viewModel),
|
|
599
650
|
"data-auth-menu-close": true,
|
|
600
651
|
disabled: recoveryNavigationLocked(viewModel),
|
|
601
652
|
onClick: this.onBackClick,
|
|
@@ -856,9 +907,9 @@ var AuthMenuSurface = class extends Component {
|
|
|
856
907
|
}
|
|
857
908
|
renderRecovery(viewModel) {
|
|
858
909
|
if (viewModel.stage === "enter_code") {
|
|
859
|
-
const
|
|
860
|
-
const
|
|
861
|
-
const
|
|
910
|
+
const code = recoveryInputCharacters(viewModel.recoveryCode).slice(0, 32);
|
|
911
|
+
const arrival = this.recoveryCodeArrival(code);
|
|
912
|
+
const rejection = this.state.recoveryRejection;
|
|
862
913
|
const statusMessage = viewModel.status.kind === "recoverable" ? viewModel.status.message : "";
|
|
863
914
|
const feedbackMessage = viewModel.recoveryCodeError ?? statusMessage;
|
|
864
915
|
const feedbackIsError = feedbackMessage.length > 0;
|
|
@@ -877,58 +928,110 @@ var AuthMenuSurface = class extends Component {
|
|
|
877
928
|
onSubmit: this.onRecoverySubmit,
|
|
878
929
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
879
930
|
class: "seams-recovery-field",
|
|
880
|
-
children: [
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
931
|
+
children: [
|
|
932
|
+
/* @__PURE__ */ jsxs("div", {
|
|
933
|
+
class: "seams-recovery-field-header",
|
|
934
|
+
children: [/* @__PURE__ */ jsx("label", {
|
|
935
|
+
class: "seams-field-label",
|
|
936
|
+
for: "seams-recovery-code",
|
|
937
|
+
children: "Recovery code"
|
|
938
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
939
|
+
id: "seams-recovery-code-count",
|
|
940
|
+
class: "seams-recovery-code-count",
|
|
941
|
+
children: [
|
|
942
|
+
code.length,
|
|
943
|
+
"/",
|
|
944
|
+
32
|
|
945
|
+
]
|
|
946
|
+
})]
|
|
947
|
+
}),
|
|
948
|
+
/* @__PURE__ */ jsxs("div", {
|
|
949
|
+
class: `seams-otp-code-field seams-recovery-code-field ${rejection ? `is-rejecting-${rejection.parity}` : ""}`,
|
|
950
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
951
|
+
id: "seams-recovery-code",
|
|
952
|
+
class: "seams-otp-input",
|
|
953
|
+
"data-recovery-code": true,
|
|
954
|
+
name: "recoveryCode",
|
|
955
|
+
type: "text",
|
|
956
|
+
inputmode: "text",
|
|
957
|
+
autocomplete: "off",
|
|
958
|
+
autocapitalize: "characters",
|
|
959
|
+
autocorrect: "off",
|
|
960
|
+
spellcheck: false,
|
|
961
|
+
"aria-invalid": viewModel.recoveryCodeError ? "true" : "false",
|
|
962
|
+
"aria-describedby": viewModel.recoveryCodeError ? "seams-recovery-code-feedback seams-recovery-code-count seams-recovery-code-hint" : "seams-recovery-code-count seams-recovery-code-hint",
|
|
963
|
+
value: code,
|
|
964
|
+
onInput: this.onRecoveryCodeInput,
|
|
965
|
+
onFocus: this.onRecoverySelectionMove,
|
|
966
|
+
onKeyUp: this.onRecoverySelectionMove,
|
|
967
|
+
onSelect: this.onRecoverySelectionMove,
|
|
968
|
+
onClick: this.onRecoveryCodeClick,
|
|
969
|
+
onBlur: this.onRecoveryCodeBlur
|
|
970
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
971
|
+
class: `seams-otp-slots seams-recovery-code-slots ${arrival && arrival.end - arrival.start > 1 ? "is-sweeping" : ""}`,
|
|
972
|
+
"aria-hidden": "true",
|
|
973
|
+
children: renderRecoveryCodeGroups(code, this.state.recoverySelection, arrival)
|
|
974
|
+
})]
|
|
975
|
+
}),
|
|
976
|
+
/* @__PURE__ */ jsx("p", {
|
|
977
|
+
id: "seams-recovery-code-hint",
|
|
978
|
+
class: `seams-recovery-code-hint ${rejection ? "is-error" : ""}`,
|
|
979
|
+
"aria-live": "polite",
|
|
980
|
+
children: rejection ? `Recovery codes don’t use ${rejection.characters.join(", ")}.` : "You can paste the whole code."
|
|
981
|
+
})
|
|
982
|
+
]
|
|
983
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
984
|
+
class: "seams-auth-method-btn seams-auth-method-btn-primary",
|
|
985
|
+
type: "submit",
|
|
986
|
+
"data-auth-menu-primary": true,
|
|
987
|
+
"data-recovery-continue": true,
|
|
988
|
+
children: "Continue"
|
|
928
989
|
})]
|
|
929
990
|
})
|
|
930
991
|
] });
|
|
931
992
|
}
|
|
993
|
+
if (viewModel.stage === "choose_method" || viewModel.stage === "preparing") {
|
|
994
|
+
const busyTarget = viewModel.stage === "preparing" ? viewModel.target.kind : null;
|
|
995
|
+
const statusMessage = viewModel.status.kind === "recoverable" ? viewModel.status.message : viewModel.status.kind === "busy" ? viewModel.status.headline : "";
|
|
996
|
+
const googleEnabled = viewModel.enabledExternalProviders?.includes("google") ?? false;
|
|
997
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
998
|
+
this.renderHeader(viewModel),
|
|
999
|
+
/* @__PURE__ */ jsx("p", {
|
|
1000
|
+
class: `seams-recovery-status ${viewModel.status.kind === "recoverable" ? "seams-recovery-error" : ""}`,
|
|
1001
|
+
"aria-hidden": "true",
|
|
1002
|
+
children: statusMessage
|
|
1003
|
+
}),
|
|
1004
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1005
|
+
class: "seams-auth-methods seams-recovery-methods",
|
|
1006
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
1007
|
+
class: "seams-auth-method-btn seams-auth-method-btn-primary",
|
|
1008
|
+
type: "button",
|
|
1009
|
+
"data-auth-menu-primary": true,
|
|
1010
|
+
"data-recovery-target": "passkey",
|
|
1011
|
+
disabled: busyTarget !== null,
|
|
1012
|
+
onClick: this.onRecoveryPasskeySelected,
|
|
1013
|
+
children: [busyTarget === "passkey" ? /* @__PURE__ */ jsx("span", {
|
|
1014
|
+
class: "seams-spinner",
|
|
1015
|
+
"aria-hidden": "true"
|
|
1016
|
+
}) : fingerprintIcon(), /* @__PURE__ */ jsx("span", { children: "Create a passkey" })]
|
|
1017
|
+
}), googleEnabled ? /* @__PURE__ */ jsxs("button", {
|
|
1018
|
+
class: "seams-auth-method-btn",
|
|
1019
|
+
type: "button",
|
|
1020
|
+
"data-recovery-target": "google_email_otp",
|
|
1021
|
+
disabled: busyTarget !== null,
|
|
1022
|
+
onClick: this.onRecoveryGoogleSelected,
|
|
1023
|
+
children: [busyTarget === "google_email_otp" ? /* @__PURE__ */ jsx("span", {
|
|
1024
|
+
class: "seams-spinner",
|
|
1025
|
+
"aria-hidden": "true"
|
|
1026
|
+
}) : googleIcon(), /* @__PURE__ */ jsx("span", { children: "Continue with Google" })]
|
|
1027
|
+
}) : null]
|
|
1028
|
+
}),
|
|
1029
|
+
/* @__PURE__ */ jsx("p", {
|
|
1030
|
+
class: "seams-recovery-method-note",
|
|
1031
|
+
children: "This adds a new sign-in method to your wallet."
|
|
1032
|
+
})
|
|
1033
|
+
] });
|
|
1034
|
+
}
|
|
932
1035
|
if (viewModel.stage === "email_code_required") {
|
|
933
1036
|
const digits = otpCodeDigits(viewModel.otpCode);
|
|
934
1037
|
const canSubmit = /^\d{6}$/.test(viewModel.otpCode) && viewModel.status.kind !== "busy";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
|
|
2
|
+
//#region src/SeamsWeb/walletIframe/host/ui/auth-menu/linkDeviceBadge.tsx
|
|
3
|
+
const LINK_DEVICE_DOT_COUNT = 12;
|
|
4
|
+
function linkDeviceDotRing(phase, glyph) {
|
|
5
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsxs("div", {
|
|
6
|
+
class: `seams-link-device-dot-ring is-${phase}`,
|
|
7
|
+
"aria-hidden": "true",
|
|
8
|
+
children: [
|
|
9
|
+
Array.from({ length: LINK_DEVICE_DOT_COUNT }, (_, index) => /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("span", { "data-dot-index": index }) })),
|
|
10
|
+
glyph ? /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
|
|
11
|
+
class: "seams-link-device-dot-ring-glyph",
|
|
12
|
+
children: glyph
|
|
13
|
+
}) }) : null,
|
|
14
|
+
/* @__PURE__ */ jsx("svg", {
|
|
15
|
+
class: "seams-link-device-dot-ring-check",
|
|
16
|
+
viewBox: "0 0 52 52",
|
|
17
|
+
fill: "none",
|
|
18
|
+
stroke: "currentColor",
|
|
19
|
+
"stroke-width": "2.5",
|
|
20
|
+
"stroke-linecap": "round",
|
|
21
|
+
"stroke-linejoin": "round",
|
|
22
|
+
children: /* @__PURE__ */ jsx("path", { d: "m18.5 27 5 5 10-11" })
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
}) });
|
|
26
|
+
}
|
|
27
|
+
function linkDeviceEmailOtpPresentation(activation) {
|
|
28
|
+
switch (activation.kind) {
|
|
29
|
+
case "sending": return {
|
|
30
|
+
ring: "waiting",
|
|
31
|
+
heading: "Sending your code",
|
|
32
|
+
status: "Emailing a 6-digit code",
|
|
33
|
+
pending: true,
|
|
34
|
+
tone: "neutral"
|
|
35
|
+
};
|
|
36
|
+
case "resending": return {
|
|
37
|
+
ring: "waiting",
|
|
38
|
+
heading: "Sending a new code",
|
|
39
|
+
status: "Emailing a fresh 6-digit code",
|
|
40
|
+
pending: true,
|
|
41
|
+
tone: "neutral"
|
|
42
|
+
};
|
|
43
|
+
case "submitting": return {
|
|
44
|
+
ring: "waiting",
|
|
45
|
+
heading: "Checking your code",
|
|
46
|
+
status: "Verifying the code you entered",
|
|
47
|
+
pending: true,
|
|
48
|
+
tone: "neutral"
|
|
49
|
+
};
|
|
50
|
+
case "code_input": return {
|
|
51
|
+
ring: "idle",
|
|
52
|
+
heading: "Verify your email",
|
|
53
|
+
status: "Enter the 6-digit code we just sent.",
|
|
54
|
+
pending: false,
|
|
55
|
+
tone: "neutral"
|
|
56
|
+
};
|
|
57
|
+
case "incorrect": return {
|
|
58
|
+
ring: "idle",
|
|
59
|
+
heading: "Verify your email",
|
|
60
|
+
status: activation.message,
|
|
61
|
+
pending: false,
|
|
62
|
+
tone: "error"
|
|
63
|
+
};
|
|
64
|
+
case "expired": return {
|
|
65
|
+
ring: "idle",
|
|
66
|
+
heading: "That code expired",
|
|
67
|
+
status: activation.message,
|
|
68
|
+
pending: false,
|
|
69
|
+
tone: "error"
|
|
70
|
+
};
|
|
71
|
+
case "unavailable": return {
|
|
72
|
+
ring: "idle",
|
|
73
|
+
heading: "Email code unavailable",
|
|
74
|
+
status: activation.message,
|
|
75
|
+
pending: false,
|
|
76
|
+
tone: "error"
|
|
77
|
+
};
|
|
78
|
+
case "completed": return {
|
|
79
|
+
ring: "approved",
|
|
80
|
+
heading: "Email verified",
|
|
81
|
+
status: "Finishing linking this device",
|
|
82
|
+
pending: true,
|
|
83
|
+
tone: "neutral"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { linkDeviceDotRing, linkDeviceEmailOtpPresentation };
|
|
89
|
+
|
|
90
|
+
//# sourceMappingURL=linkDeviceBadge.js.map
|