@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.
Files changed (58) hide show
  1. package/README.md +3 -3
  2. package/dist/.build-inputs.sha256 +1 -1
  3. package/dist/esm/SeamsWeb/walletIframe/host/ui/auth-menu/AuthMenuSurface.js +252 -149
  4. package/dist/esm/SeamsWeb/walletIframe/host/ui/auth-menu/linkDeviceBadge.js +90 -0
  5. package/dist/esm/sdk/{host-Bqmmy8Ts.js → host-GawN20iA.js} +1 -1
  6. package/dist/{public/sdk/pendingWalletRecoveryCodeBackup-CMCOUuA-.js → esm/sdk/pendingWalletRecoveryCodeBackup-DMg5U0Fk.js} +1 -1
  7. package/dist/{public/sdk/recovery-entrypoint-QxTiyx-o.js → esm/sdk/recovery-entrypoint-EHY_b7Z-.js} +1 -1
  8. package/dist/{public/sdk/registration-eyzHqLHi.js → esm/sdk/registration-CQxxUt5s.js} +1 -1
  9. package/dist/esm/sdk/runtime-auth-TZw6JD8N.js +1 -0
  10. package/dist/esm/sdk/{runtime-device-link-CHsqrEQ9.js → runtime-device-link-DXjabXJb.js} +1 -1
  11. package/dist/esm/sdk/{runtime-ecdsa-tempo-MgmnU9g-.js → runtime-ecdsa-tempo-DlKMIbyP.js} +1 -1
  12. package/dist/esm/sdk/{runtime-email-otp-Cj96n102.js → runtime-email-otp-BBPghU2G.js} +1 -1
  13. package/dist/esm/sdk/{runtime-export-mcwZRxKk.js → runtime-export-pw-xJL8o.js} +1 -1
  14. package/dist/esm/sdk/{runtime-near-BMdTtu7D.js → runtime-near-B-vYxGjl.js} +1 -1
  15. package/dist/esm/sdk/{runtime-preferences-CBlhE8bX.js → runtime-preferences-D-hY0RfC.js} +1 -1
  16. package/dist/esm/sdk/{runtime-recovery-mJp3MLeG.js → runtime-recovery-Vg1ND2nl.js} +1 -1
  17. package/dist/esm/sdk/{runtime-recovery-codes-B1gixZsp.js → runtime-recovery-codes-rLR4Guy4.js} +1 -1
  18. package/dist/esm/sdk/{shared-8Qe7Nxgg.js → shared-6bpRPbcM.js} +1 -1
  19. package/dist/esm/sdk/wallet-iframe-host-ecdsa.js +1 -1
  20. package/dist/esm/sdk/wallet-iframe-host-full.js +1 -1
  21. package/dist/esm/sdk/wallet-iframe-host-near.js +1 -1
  22. package/dist/esm/sdk/wallet-iframe-host-runtime.js +1 -1
  23. package/dist/esm/sdk/{wallet-recovery-D7ywcIHw.js → wallet-recovery-LMtGiEL3.js} +1 -1
  24. package/dist/esm/sdk/wallet-ui.css +1 -1
  25. package/dist/esm/sdk/{walletCustodyEcdsaContinuity-_IKoCUBO.js → walletCustodyEcdsaContinuity-D-gYxv6e.js} +1 -1
  26. package/dist/esm/sdk/{walletRecoveryCommit-DDPa1rAJ.js → walletRecoveryCommit-BYRC_2li.js} +1 -1
  27. package/dist/public/headers.manifest.json +1 -1
  28. package/dist/public/sdk/{host-Bqmmy8Ts.js → host-GawN20iA.js} +1 -1
  29. package/dist/{esm/sdk/pendingWalletRecoveryCodeBackup-CMCOUuA-.js → public/sdk/pendingWalletRecoveryCodeBackup-DMg5U0Fk.js} +1 -1
  30. package/dist/{esm/sdk/recovery-entrypoint-QxTiyx-o.js → public/sdk/recovery-entrypoint-EHY_b7Z-.js} +1 -1
  31. package/dist/{esm/sdk/registration-eyzHqLHi.js → public/sdk/registration-CQxxUt5s.js} +1 -1
  32. package/dist/public/sdk/runtime-auth-TZw6JD8N.js +1 -0
  33. package/dist/public/sdk/{runtime-device-link-CHsqrEQ9.js → runtime-device-link-DXjabXJb.js} +1 -1
  34. package/dist/public/sdk/{runtime-ecdsa-tempo-MgmnU9g-.js → runtime-ecdsa-tempo-DlKMIbyP.js} +1 -1
  35. package/dist/public/sdk/{runtime-email-otp-Cj96n102.js → runtime-email-otp-BBPghU2G.js} +1 -1
  36. package/dist/public/sdk/{runtime-export-mcwZRxKk.js → runtime-export-pw-xJL8o.js} +1 -1
  37. package/dist/public/sdk/{runtime-near-BMdTtu7D.js → runtime-near-B-vYxGjl.js} +1 -1
  38. package/dist/public/sdk/{runtime-preferences-CBlhE8bX.js → runtime-preferences-D-hY0RfC.js} +1 -1
  39. package/dist/public/sdk/{runtime-recovery-mJp3MLeG.js → runtime-recovery-Vg1ND2nl.js} +1 -1
  40. package/dist/public/sdk/{runtime-recovery-codes-B1gixZsp.js → runtime-recovery-codes-rLR4Guy4.js} +1 -1
  41. package/dist/public/sdk/{shared-8Qe7Nxgg.js → shared-6bpRPbcM.js} +1 -1
  42. package/dist/public/sdk/wallet-iframe-host-ecdsa.js +1 -1
  43. package/dist/public/sdk/wallet-iframe-host-full.js +1 -1
  44. package/dist/public/sdk/wallet-iframe-host-near.js +1 -1
  45. package/dist/public/sdk/wallet-iframe-host-runtime.js +1 -1
  46. package/dist/public/sdk/{wallet-recovery-D7ywcIHw.js → wallet-recovery-LMtGiEL3.js} +1 -1
  47. package/dist/public/sdk/wallet-ui.css +1 -1
  48. package/dist/public/sdk/{walletCustodyEcdsaContinuity-_IKoCUBO.js → walletCustodyEcdsaContinuity-D-gYxv6e.js} +1 -1
  49. package/dist/public/sdk/{walletRecoveryCommit-DDPa1rAJ.js → walletRecoveryCommit-BYRC_2li.js} +1 -1
  50. package/dist/public/wallet-assets.manifest.json +38 -38
  51. package/dist/public/wallet-service/index.html +4 -4
  52. package/dist/types/wallet/src/SeamsWeb/walletIframe/host/auth-menu/domain.d.ts +7 -3
  53. package/dist/types/wallet/src/SeamsWeb/walletIframe/host/auth-menu/session.d.ts +3 -1
  54. package/dist/types/wallet/src/SeamsWeb/walletIframe/host/ui/auth-menu/AuthMenuSurface.d.ts +26 -5
  55. package/dist/types/wallet/src/SeamsWeb/walletIframe/host/ui/auth-menu/linkDeviceBadge.d.ts +18 -0
  56. package/package.json +1 -3
  57. package/dist/esm/sdk/runtime-auth-CbveTM2_.js +0 -1
  58. 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.11.0** provides browser wallet lifecycle, NEAR and EVM-family
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.11.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.11.0` on the matching release.
15
+ `@seams/wallet-server@0.13.0` on the matching release.
16
16
 
17
17
  ## Next release migration (unreleased)
18
18
 
@@ -1 +1 @@
1
- 6d2e6b22efe3f00372c471ea3e8fa79ef5f85a847e71a1b1fffffb303b5d669f
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
- return code.replace(/[\s\-\u2010-\u2015\u2212]/g, "").toUpperCase();
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 recoveryCodeGroups(digits) {
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 index = 0; index < digits.length; index += 4) groups.push(digits.slice(index, index + 4));
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 renderRecoveryCodeGroup(digits, index) {
35
- return /* @__PURE__ */ jsx("div", {
36
- class: "seams-recovery-code-group",
37
- children: digits.map(renderOtpCodeDigit)
38
- }, index);
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 = { accountMenu: "closed" };
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
- const viewModel = this.viewModel;
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, length);
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": recovery ? "Back to unlock wallet" : "Back",
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 codeLength = viewModel.recoveryCodeSlotCount ?? 32;
860
- const code = recoveryInputCharacters(viewModel.recoveryCode).slice(0, codeLength);
861
- const digits = otpCodeDigits(code, codeLength);
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: [/* @__PURE__ */ jsx("label", {
881
- class: "seams-field-label",
882
- for: "seams-recovery-code",
883
- children: "Recovery code"
884
- }), /* @__PURE__ */ jsxs("div", {
885
- class: "seams-otp-code-field seams-recovery-code-field",
886
- children: [/* @__PURE__ */ jsx("input", {
887
- id: "seams-recovery-code",
888
- class: "seams-otp-input",
889
- "data-recovery-code": true,
890
- name: "recoveryCode",
891
- type: "text",
892
- inputmode: codeLength === 16 ? "numeric" : "text",
893
- autocomplete: "off",
894
- autocapitalize: "characters",
895
- autocorrect: "off",
896
- spellcheck: false,
897
- "aria-invalid": viewModel.recoveryCodeError ? "true" : "false",
898
- "aria-describedby": viewModel.recoveryCodeError ? "seams-recovery-code-feedback" : void 0,
899
- value: code,
900
- onInput: this.onRecoveryCodeInput
901
- }), /* @__PURE__ */ jsx("div", {
902
- class: "seams-otp-slots seams-recovery-code-slots",
903
- "aria-hidden": "true",
904
- children: recoveryCodeGroups(digits).map(renderRecoveryCodeGroup)
905
- })]
906
- })]
907
- }), /* @__PURE__ */ jsxs("div", {
908
- class: "seams-secondary-actions",
909
- children: [/* @__PURE__ */ jsxs("button", {
910
- class: "seams-link-device-btn seams-auth-method-choice-btn",
911
- type: "button",
912
- "data-recovery-target": "passkey",
913
- onClick: this.onRecoveryPasskeySelected,
914
- children: [/* @__PURE__ */ jsx("span", {
915
- class: "seams-auth-method-choice-icon",
916
- children: fingerprintIcon()
917
- }), /* @__PURE__ */ jsx("span", { children: "Recover with Passkey" })]
918
- }), /* @__PURE__ */ jsxs("button", {
919
- class: "seams-link-device-btn seams-auth-method-choice-btn",
920
- type: "button",
921
- "data-recovery-target": "google_email_otp",
922
- onClick: this.onRecoveryGoogleSelected,
923
- children: [/* @__PURE__ */ jsx("span", {
924
- class: "seams-auth-method-choice-icon",
925
- children: googleIcon()
926
- }), /* @__PURE__ */ jsx("span", { children: "Recover with Google" })]
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