viveworker 0.8.5 → 0.8.7

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/web/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "./i18n.js";
1
+ import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "./i18n.js?v=__VIVEWORKER_APP_BUILD_ID__";
2
2
  import { ensureIdentityKeypair, bytesToHex } from "./remote-pairing/keys.js";
3
3
  import {
4
4
  loadPairingState as loadRemotePairingState,
@@ -51,6 +51,7 @@ const TIMELINE_STICKY_LAN_PROBE_TIMEOUT_MS = 350;
51
51
  const CLIENT_EVENT_REPORT_TIMEOUT_MS = 1_800;
52
52
  const TIMELINE_LIVE_REFRESH_TIMEOUT_MS = 2_000;
53
53
  const TIMELINE_LIVE_RETRY_MS = 5_000;
54
+ const HAZBASE_ACTION_TIMEOUT_MS = 30_000;
54
55
  const timelineImageObjectUrlCache = new Map();
55
56
 
56
57
  function wait(ms) {
@@ -141,12 +142,11 @@ const state = {
141
142
  hazbaseStatus: null,
142
143
  hazbaseNotice: "",
143
144
  hazbaseError: "",
144
- // Session-only flag: once the Sepolia wallet is ready, the mainnet step
145
- // is hidden behind a subtle opt-in link (most beta users won't activate
146
- // it). Flipping this reveals the full mainnet step card for the rest of
147
- // the session. Not persisted — reopening Wallet next visit starts hidden
148
- // again, which is the desired default for the closed beta.
149
- hazbaseMainnetOptIn: false,
145
+ hazbaseFormErrors: { email: "", otp: "", liquid: {} },
146
+ hazbasePendingAction: "",
147
+ hazbaseAgentPaymentDefaultsDraft: null,
148
+ agentPaymentDefaultsModalOpen: false,
149
+ toast: null,
150
150
  // Sign-in OTP flow is a two-step send→verify dance. Showing both
151
151
  // buttons side-by-side confused users; they clicked "Verify" before
152
152
  // ever requesting a code. We gate the verify button behind a
@@ -202,6 +202,7 @@ let timelineLiveRefreshInFlight = false;
202
202
  let timelineLiveRefreshPending = false;
203
203
  let lastTimelineLiveRevision = 0;
204
204
  const reportedTimelineRenderTokens = new Set();
205
+ let toastDismissTimer = 0;
205
206
 
206
207
  async function loadHazbasePasskeyModule() {
207
208
  if (!hazbasePasskeyModulePromise) {
@@ -1223,9 +1224,9 @@ async function fetchA2aShareStatus() {
1223
1224
  }
1224
1225
 
1225
1226
 
1226
- async function fetchHazbaseStatus() {
1227
+ async function fetchHazbaseStatus(opts = {}) {
1227
1228
  try {
1228
- state.hazbaseStatus = await apiGet("/api/hazbase/status");
1229
+ state.hazbaseStatus = await apiGet("/api/hazbase/status", opts);
1229
1230
  } catch {
1230
1231
  state.hazbaseStatus = null;
1231
1232
  }
@@ -2363,6 +2364,8 @@ async function renderShell() {
2363
2364
  ${renderInstallGuideModal()}
2364
2365
  ${renderLogoutConfirmModal()}
2365
2366
  ${renderHazbaseLogoutConfirmModal()}
2367
+ ${renderAgentPaymentDefaultsModal()}
2368
+ ${renderToastLayer()}
2366
2369
  </div>
2367
2370
  ${!desktop && detail ? renderCompletionReplySheet(detail) : ""}
2368
2371
  `;
@@ -2919,6 +2922,37 @@ function renderMobileTopBar(detail) {
2919
2922
  `;
2920
2923
  }
2921
2924
 
2925
+ function showToast(message, { tone = "success" } = {}) {
2926
+ const text = String(message || "").trim();
2927
+ if (!text) return;
2928
+ const normalizedTone = tone === "error" ? "error" : "success";
2929
+ const id = `${Date.now()}:${Math.random().toString(36).slice(2)}`;
2930
+ state.toast = { id, message: text, tone: normalizedTone };
2931
+ if (toastDismissTimer) {
2932
+ clearTimeout(toastDismissTimer);
2933
+ }
2934
+ toastDismissTimer = setTimeout(() => {
2935
+ if (state.toast?.id !== id) {
2936
+ return;
2937
+ }
2938
+ state.toast = null;
2939
+ renderCurrentSurface();
2940
+ }, normalizedTone === "error" ? 5600 : 3800);
2941
+ }
2942
+
2943
+ function renderToastLayer() {
2944
+ if (!state.toast) {
2945
+ return "";
2946
+ }
2947
+ const tone = state.toast.tone === "error" ? "error" : "success";
2948
+ return `
2949
+ <div class="app-toast app-toast--${escapeHtml(tone)}" role="${tone === "error" ? "alert" : "status"}" aria-live="${tone === "error" ? "assertive" : "polite"}">
2950
+ <span class="app-toast__icon" aria-hidden="true">${renderIcon(tone === "success" ? "completed" : "lock")}</span>
2951
+ <span class="app-toast__message">${escapeHtml(state.toast.message)}</span>
2952
+ </div>
2953
+ `;
2954
+ }
2955
+
2922
2956
  function renderableCurrentDetail(itemRef = state.currentItem) {
2923
2957
  if (!itemRef) {
2924
2958
  return null;
@@ -4764,7 +4798,7 @@ function settingsWalletRootValue(context) {
4764
4798
  if (context.hazbase?.enabled !== true) {
4765
4799
  return L("common.disabled");
4766
4800
  }
4767
- return deriveHazbaseWalletFlow(context.hazbase).coreReady
4801
+ return effectiveAgentPaymentDefaults(context.hazbase).length > 0
4768
4802
  ? L("common.enabled")
4769
4803
  : settingsNeedsActionValue();
4770
4804
  }
@@ -4925,6 +4959,34 @@ function settingsPageMeta(page) {
4925
4959
  description: L("settings.wallet.copy"),
4926
4960
  icon: "coin",
4927
4961
  };
4962
+ case "walletInventory":
4963
+ return {
4964
+ id: "walletInventory",
4965
+ title: L("settings.wallet.inventory.title"),
4966
+ description: L("settings.wallet.inventory.copy"),
4967
+ icon: "coin",
4968
+ };
4969
+ case "walletInventoryBase":
4970
+ return {
4971
+ id: "walletInventoryBase",
4972
+ title: L("settings.wallet.chain.base.title"),
4973
+ description: L("settings.wallet.chain.base.copy"),
4974
+ icon: "coin",
4975
+ };
4976
+ case "walletInventoryLiquid":
4977
+ return {
4978
+ id: "walletInventoryLiquid",
4979
+ title: L("settings.wallet.chain.liquid.title"),
4980
+ description: L("settings.wallet.chain.liquid.copy"),
4981
+ icon: "coin",
4982
+ };
4983
+ case "walletInventoryPolygon":
4984
+ return {
4985
+ id: "walletInventoryPolygon",
4986
+ title: L("settings.wallet.chain.polygon.title"),
4987
+ description: L("settings.wallet.chain.polygon.copy"),
4988
+ icon: "coin",
4989
+ };
4928
4990
  case "remotePairing":
4929
4991
  return {
4930
4992
  id: "remotePairing",
@@ -5102,6 +5164,18 @@ function renderSettingsSubpage(context, { mobile }) {
5102
5164
  case "wallet":
5103
5165
  content = renderSettingsWalletPage(context);
5104
5166
  break;
5167
+ case "walletInventory":
5168
+ content = renderSettingsWalletInventoryPage(context);
5169
+ break;
5170
+ case "walletInventoryBase":
5171
+ content = renderSettingsWalletChainPage(context, "base");
5172
+ break;
5173
+ case "walletInventoryLiquid":
5174
+ content = renderSettingsWalletChainPage(context, "liquid");
5175
+ break;
5176
+ case "walletInventoryPolygon":
5177
+ content = renderSettingsWalletChainPage(context, "polygon");
5178
+ break;
5105
5179
  case "remotePairing":
5106
5180
  content = renderSettingsRemotePairingPage(context);
5107
5181
  break;
@@ -6055,13 +6129,19 @@ function renderSettingsA2aSharePage(context) {
6055
6129
  const lock = item.hasPassword
6056
6130
  ? `<span class="settings-compose-badge settings-compose-badge--reply" role="img" title="${escapeHtml(passwordLabel)}" aria-label="${escapeHtml(passwordLabel)}">${renderIcon("lock")}</span>`
6057
6131
  : "";
6058
- // Paid-share badge. `price` is atomic USDC (6-decimals) on the item.
6132
+ // Paid-share badge. `price` is atomic units for the primary payment option.
6059
6133
  // Mutually exclusive with hasPassword at upload time, so the two badges
6060
6134
  // never both render, but the HTML doesn't assume that — it just renders
6061
6135
  // whichever are set.
6136
+ const primaryPaymentOption = Array.isArray(item.paymentOptions) && item.paymentOptions.length > 0
6137
+ ? item.paymentOptions[0]
6138
+ : null;
6139
+ const paidAsset = paymentAssetLabel(primaryPaymentOption?.asset || item.asset || "usdc");
6140
+ const paidDecimals = Number(primaryPaymentOption?.decimals || paymentAssetDecimals(primaryPaymentOption?.asset || item.asset || "usdc"));
6062
6141
  const paidLabel = item.price
6063
6142
  ? L("settings.a2aShare.paidShare", {
6064
- price: formatUsdcAtomic(item.price),
6143
+ price: formatAtomicAmount(item.price, paidDecimals),
6144
+ asset: paidAsset,
6065
6145
  network: item.network || "?",
6066
6146
  })
6067
6147
  : "";
@@ -6388,51 +6468,26 @@ function renderSettingsWalletPage(context) {
6388
6468
  </div>
6389
6469
  `;
6390
6470
  }
6391
- const flow = deriveHazbaseWalletFlow(hazbase);
6392
-
6393
- // Progressive disclosure. Previous layout rendered the full status summary
6394
- // plus all four full-sized step cards at once; new account flows were
6395
- // dominated by locked-looking cards and it was hard to tell which step was
6396
- // actionable. Now we:
6397
- // - skip `locked` steps entirely (still-unreachable actions add noise),
6398
- // - collapse `complete` steps to a compact one-line row that keeps the
6399
- // verified value visible (email / address) without a full card,
6400
- // - keep the single `current`/`pending` step in the full action card so
6401
- // the CTA is unambiguous,
6402
- // - hide the optional mainnet step behind a subtle opt-in link until the
6403
- // user explicitly requests it (see `state.hazbaseMainnetOptIn`).
6404
- // The compact rows also replace the former "Current status" summary group
6405
- // above the flow, so signed-in email / passkey state / addresses are no
6406
- // longer duplicated.
6407
- const guideRows = [
6408
- renderHazbaseWalletBanner(flow),
6409
- renderHazbaseWalletBetaNotice(),
6410
- state.hazbaseNotice
6411
- ? `<div class="settings-copy-block settings-copy-block--compact wallet-flow-message wallet-flow-message--notice"><p>${escapeHtml(state.hazbaseNotice)}</p></div>`
6412
- : "",
6413
- state.hazbaseError
6414
- ? `<div class="settings-copy-block settings-copy-block--compact wallet-flow-message wallet-flow-message--error"><p>${escapeHtml(state.hazbaseError)}</p></div>`
6415
- : "",
6416
- renderHazbaseWalletStepList(flow),
6417
- ].filter(Boolean);
6418
- const canRefreshSession = Boolean(hazbase.sessionInvalid);
6419
- const advancedActions = canRefreshSession || hazbase.signedIn
6420
- ? [
6421
- canRefreshSession
6422
- ? `<button class="secondary secondary--wide" type="button" data-hazbase-action="refresh-session">${escapeHtml(L("settings.hazbase.action.refreshSession"))}</button>`
6423
- : "",
6424
- hazbase.signedIn
6425
- ? `<button class="secondary secondary--wide" type="button" data-hazbase-action="logout">${escapeHtml(L("settings.hazbase.action.signOut"))}</button>`
6426
- : "",
6427
- ].filter(Boolean).join("")
6471
+ const configured = configuredPaymentCapabilities(hazbase);
6472
+ const configuredNetworkCount = configuredPaymentCapabilityNetworkCount(hazbase);
6473
+ const agentConfigured = agentEligiblePaymentCapabilities(hazbase);
6474
+ const effectiveDefaults = effectiveAgentPaymentDefaults(hazbase);
6475
+ const defaultsSummary = renderAgentPaymentDefaultsSummary(hazbase, effectiveDefaults);
6476
+ const agentEmptyKey = configured.length ? "settings.wallet.agent.emptyAvailable" : "settings.wallet.agent.empty";
6477
+ const defaultsRows = agentConfigured.length
6478
+ ? agentConfigured.map((capability) => renderAgentPaymentDefaultChoice(hazbase, capability))
6479
+ : [`<div class="settings-copy-block settings-copy-block--compact"><p class="muted">${escapeHtml(L(agentEmptyKey))}</p></div>`];
6480
+ const defaultsDraftNotice = state.hazbaseAgentPaymentDefaultsDraft
6481
+ ? `<div class="settings-copy-block settings-copy-block--compact wallet-agent-draft-notice"><p>${escapeHtml(L("settings.wallet.agent.unsaved"))}</p></div>`
6482
+ : "";
6483
+ const defaultsActions = agentConfigured.length
6484
+ ? `
6485
+ <div class="wallet-agent-actions">
6486
+ <button class="primary primary--wide${state.hazbasePendingAction === "agent-defaults-save" ? " is-loading" : ""}" type="button" data-hazbase-agent-defaults-save ${state.hazbasePendingAction ? "disabled" : ""} ${state.hazbasePendingAction === "agent-defaults-save" ? 'aria-busy="true"' : ""}>${escapeHtml(L("settings.wallet.agent.save"))}</button>
6487
+ <button class="secondary secondary--wide${state.hazbasePendingAction === "agent-defaults-all" ? " is-loading" : ""}" type="button" data-hazbase-agent-defaults-all ${state.hazbasePendingAction ? "disabled" : ""} ${state.hazbasePendingAction === "agent-defaults-all" ? 'aria-busy="true"' : ""}>${escapeHtml(L("settings.wallet.agent.useAllConfigured"))}</button>
6488
+ </div>
6489
+ `
6428
6490
  : "";
6429
- // Render the wallet flow without `renderSettingsGroup`'s `.settings-list`
6430
- // wrapper. The banner (`.settings-copy-block`), notice/error blocks, and
6431
- // each step card (`.wallet-step-card`) already have their own rounded
6432
- // frame — wrapping them in another `.settings-list` produced a visible
6433
- // "box inside a box" nest. The title (`.settings-group__title`) still
6434
- // sits above a flat stack of sibling cards via `.wallet-setup-stack`.
6435
- const flowTitle = L("settings.wallet.flow.title");
6436
6491
  // Brand attribution. The wallet stack (factory + validator + bundler +
6437
6492
  // paymaster) is provided by hazBase; the link points to their LP so
6438
6493
  // curious users can discover what's running the signing path.
@@ -6443,82 +6498,583 @@ function renderSettingsWalletPage(context) {
6443
6498
  `;
6444
6499
  return `
6445
6500
  <div class="settings-page">
6446
- <section class="settings-group">
6447
- ${flowTitle ? `<p class="settings-group__title">${escapeHtml(flowTitle)}</p>` : ""}
6448
- <div class="wallet-setup-stack">
6449
- ${guideRows.join("")}
6450
- </div>
6451
- </section>
6452
- ${advancedActions ? renderSettingsActionPanel(advancedActions, L("settings.wallet.advanced.title")) : ""}
6501
+ ${renderHazbaseWalletMessages()}
6502
+ ${renderHazbaseWalletBetaNotice()}
6503
+ ${renderSettingsGroup(L("settings.wallet.summary.title"), [
6504
+ renderSettingsInfoRow(L("settings.row.hazbaseStatus"), hazbase.signedIn ? L("settings.hazbase.status.signedIn") : L("settings.hazbase.status.signedOut"), {
6505
+ valueTone: hazbase.signedIn ? "enabled" : "attention",
6506
+ }),
6507
+ renderSettingsInfoRow(L("settings.wallet.agent.currentDefaults"), defaultsSummary.value, {
6508
+ rawValue: defaultsSummary.rawValue,
6509
+ stacked: defaultsSummary.stacked,
6510
+ valueClassName: defaultsSummary.valueClassName,
6511
+ valueTone: defaultsSummary.valueTone,
6512
+ }),
6513
+ ], { listClassName: "settings-list settings-list--compact" })}
6514
+ ${renderSettingsGroup(L("settings.wallet.agent.title"), [
6515
+ `<div class="settings-copy-block settings-copy-block--compact"><p class="muted">${escapeHtml(L("settings.wallet.agent.copy"))}</p></div>`,
6516
+ ...defaultsRows,
6517
+ defaultsDraftNotice,
6518
+ defaultsActions,
6519
+ ], { listClassName: "settings-list settings-list--compact" })}
6520
+ ${renderSettingsGroup(L("settings.wallet.inventory.navGroup"), [
6521
+ renderSettingsNavRow({
6522
+ page: "walletInventory",
6523
+ icon: "coin",
6524
+ title: L("settings.wallet.inventory.navTitle"),
6525
+ subtitle: L("settings.wallet.inventory.navCopy"),
6526
+ value: L("settings.wallet.inventory.navValue", { count: configuredNetworkCount }),
6527
+ valueTone: configuredNetworkCount ? "enabled" : "attention",
6528
+ }),
6529
+ ], { listClassName: "settings-list settings-list--compact" })}
6453
6530
  ${poweredBy}
6454
6531
  </div>
6455
6532
  `;
6456
6533
  }
6457
6534
 
6458
- function renderHazbaseWalletStepList(flow) {
6459
- const rendered = [];
6460
- for (const step of flow.steps) {
6461
- // Keep the Base mainnet roadmap visible only after the usable Base Sepolia
6462
- // wallet is ready. Showing Step 4 during email/passkey setup makes the
6463
- // sequential flow feel like there is another action competing for focus.
6464
- if (step.number === 4 && step.status === "comingSoon" && !flow.coreReady) {
6465
- continue;
6535
+ function renderHazbaseAccountActions(hazbase) {
6536
+ const canRefreshSession = Boolean(hazbase?.sessionInvalid);
6537
+ const actions = canRefreshSession || hazbase?.signedIn
6538
+ ? [
6539
+ canRefreshSession
6540
+ ? `<button class="secondary secondary--wide${isHazbaseActionPending("refresh-session") ? " is-loading" : ""}" type="button" data-hazbase-action="refresh-session" ${state.hazbasePendingAction ? "disabled" : ""} ${isHazbaseActionPending("refresh-session") ? 'aria-busy="true"' : ""}>${escapeHtml(L("settings.hazbase.action.refreshSession"))}</button>`
6541
+ : "",
6542
+ hazbase?.signedIn
6543
+ ? `<button class="secondary secondary--wide" type="button" data-hazbase-action="logout" ${state.hazbasePendingAction ? "disabled" : ""}>${escapeHtml(L("settings.hazbase.action.signOut"))}</button>`
6544
+ : "",
6545
+ ].filter(Boolean).join("")
6546
+ : "";
6547
+ return actions ? renderSettingsActionPanel(actions, L("settings.wallet.advanced.title")) : "";
6548
+ }
6549
+
6550
+ function renderSettingsWalletInventoryPage(context) {
6551
+ const hazbase = context.hazbase || { enabled: false };
6552
+ if (!hazbase?.enabled) {
6553
+ return `
6554
+ <div class="settings-page">
6555
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.wallet.unavailable"))}</p>
6556
+ </div>
6557
+ `;
6558
+ }
6559
+ const flow = deriveHazbaseWalletFlow(hazbase);
6560
+ const authCards = flow.steps
6561
+ .filter((step) => step.kind === "auth")
6562
+ .filter((step) => step.status !== "locked")
6563
+ .map((step) => renderHazbaseWalletStepCard(step, { mode: step.status === "complete" ? "compact" : "full" }));
6564
+ const chainRows = Object.entries(paymentCapabilityChainDefinitions()).map(([chain, definition]) => renderSettingsNavRow({
6565
+ page: definition.page,
6566
+ icon: "coin",
6567
+ title: L(definition.titleKey),
6568
+ subtitle: L(definition.copyKey),
6569
+ value: paymentCapabilityChainValue(hazbase, chain),
6570
+ valueTone: configuredPaymentCapabilityNetworkCount(hazbase, definition.networks) ? "enabled" : "attention",
6571
+ }));
6572
+ return `
6573
+ <div class="settings-page">
6574
+ ${renderHazbaseWalletMessages()}
6575
+ ${authCards.length ? renderSettingsGroup(L("settings.wallet.inventory.accountTitle"), [
6576
+ `<div class="wallet-setup-stack">${authCards.join("")}</div>`,
6577
+ ], { listClassName: "settings-list settings-list--flat" }) : ""}
6578
+ ${renderHazbaseAccountActions(hazbase)}
6579
+ ${renderSettingsGroup(L("settings.wallet.inventory.issueTitle"), chainRows, { listClassName: "settings-list settings-list--compact" })}
6580
+ </div>
6581
+ `;
6582
+ }
6583
+
6584
+ function renderSettingsWalletChainPage(context, chain) {
6585
+ const hazbase = context.hazbase || { enabled: false };
6586
+ if (!hazbase?.enabled) {
6587
+ return `
6588
+ <div class="settings-page">
6589
+ <p class="settings-page-copy muted">${escapeHtml(L("settings.wallet.unavailable"))}</p>
6590
+ </div>
6591
+ `;
6592
+ }
6593
+ const definition = paymentCapabilityChainDefinition(chain);
6594
+ const networks = definition?.networks || [];
6595
+ const testnetCards = networks
6596
+ .filter((network) => paymentCapabilityDefinition(network)?.environment === "testnet")
6597
+ .map((network) => renderWalletInventoryCapabilityCard(hazbase, network));
6598
+ const mainnetCards = networks
6599
+ .filter((network) => paymentCapabilityDefinition(network)?.environment === "mainnet")
6600
+ .map((network) => renderWalletInventoryCapabilityCard(hazbase, network));
6601
+ return `
6602
+ <div class="settings-page">
6603
+ ${renderHazbaseWalletMessages()}
6604
+ ${testnetCards.length ? renderSettingsGroup(L("settings.wallet.inventory.testnetTitle"), [
6605
+ `<div class="wallet-setup-stack">${testnetCards.join("")}</div>`,
6606
+ ], { listClassName: "settings-list settings-list--flat" }) : ""}
6607
+ ${mainnetCards.length ? renderSettingsGroup(L("settings.wallet.inventory.mainnetTitle"), [
6608
+ `<div class="wallet-setup-stack">${mainnetCards.join("")}</div>`,
6609
+ ], { listClassName: "settings-list settings-list--flat" }) : ""}
6610
+ </div>
6611
+ `;
6612
+ }
6613
+
6614
+ function renderHazbaseWalletMessages() {
6615
+ return "";
6616
+ }
6617
+
6618
+ function resetHazbaseFormErrors() {
6619
+ state.hazbaseFormErrors = { email: "", otp: "", liquid: {} };
6620
+ }
6621
+
6622
+ function setHazbaseFormError(field, message, network = "") {
6623
+ const formErrors = state.hazbaseFormErrors || { email: "", otp: "", liquid: {} };
6624
+ if (field === "email") {
6625
+ state.hazbaseFormErrors = { ...formErrors, email: message };
6626
+ return;
6627
+ }
6628
+ if (field === "otp") {
6629
+ state.hazbaseFormErrors = { ...formErrors, otp: message };
6630
+ return;
6631
+ }
6632
+ if (field === "liquid") {
6633
+ state.hazbaseFormErrors = {
6634
+ ...formErrors,
6635
+ liquid: {
6636
+ ...(formErrors.liquid || {}),
6637
+ [network]: message,
6638
+ },
6639
+ };
6640
+ }
6641
+ }
6642
+
6643
+ function clearHazbaseFormError(field, network = "") {
6644
+ setHazbaseFormError(field, "", network);
6645
+ }
6646
+
6647
+ function clearHazbaseFormErrorsForAction(action, network = "") {
6648
+ if (action === "request-otp") {
6649
+ clearHazbaseFormError("email");
6650
+ } else if (action === "verify-otp") {
6651
+ clearHazbaseFormError("email");
6652
+ clearHazbaseFormError("otp");
6653
+ } else if (action === "save-liquid-capability") {
6654
+ clearHazbaseFormError("liquid", network);
6655
+ }
6656
+ }
6657
+
6658
+ function hazbaseFormError(field, network = "") {
6659
+ const formErrors = state.hazbaseFormErrors || {};
6660
+ if (field === "liquid") {
6661
+ return formErrors.liquid?.[network] || "";
6662
+ }
6663
+ return formErrors[field] || "";
6664
+ }
6665
+
6666
+ function applyHazbaseInlineError(action, error, network = "") {
6667
+ const key = error?.errorKey || "";
6668
+ const message = error?.message || String(error);
6669
+ if (action === "request-otp" && (key === "email-required" || message === L("error.hazbaseEmailRequired"))) {
6670
+ setHazbaseFormError("email", L("error.hazbaseEmailRequired"));
6671
+ return true;
6672
+ }
6673
+ if (action === "verify-otp") {
6674
+ if (key === "email-required" || message === L("error.hazbaseEmailRequired")) {
6675
+ setHazbaseFormError("email", L("error.hazbaseEmailRequired"));
6676
+ return true;
6466
6677
  }
6678
+ if (key === "otp-required" || message === L("error.hazbaseOtpRequired")) {
6679
+ setHazbaseFormError("otp", L("error.hazbaseOtpRequired"));
6680
+ return true;
6681
+ }
6682
+ }
6683
+ if (action === "save-liquid-capability") {
6684
+ if (key === "invalid-liquid-address") {
6685
+ setHazbaseFormError("liquid", L("error.hazbaseLiquidAddressInvalid"), network);
6686
+ return true;
6687
+ }
6688
+ if (message === L("error.hazbaseLiquidAddressRequired")) {
6689
+ setHazbaseFormError("liquid", L("error.hazbaseLiquidAddressRequired"), network);
6690
+ return true;
6691
+ }
6692
+ }
6693
+ return false;
6694
+ }
6467
6695
 
6468
- // Locked steps don't render. Revealing them only adds grayed-out
6469
- // placeholders below the active step; users mistake the placeholder
6470
- // status chips for inactive buttons.
6471
- if (step.status === "locked") {
6472
- continue;
6696
+ function paymentCapabilityDefinitions() {
6697
+ return {
6698
+ "base-sepolia": {
6699
+ environment: "testnet",
6700
+ family: "evm",
6701
+ asset: "usdc",
6702
+ titleKey: "settings.wallet.step.baseSepolia.title",
6703
+ copyKey: "settings.wallet.step.baseSepolia.copy",
6704
+ action: "bootstrap-base-sepolia",
6705
+ },
6706
+ base: {
6707
+ environment: "mainnet",
6708
+ family: "evm",
6709
+ asset: "usdc",
6710
+ titleKey: "settings.wallet.step.base.title",
6711
+ copyKey: "settings.wallet.step.base.copy",
6712
+ action: "bootstrap-base",
6713
+ releaseStatus: "comingSoon",
6714
+ },
6715
+ "polygon-amoy": {
6716
+ environment: "testnet",
6717
+ family: "evm",
6718
+ asset: "usdc",
6719
+ titleKey: "settings.wallet.step.polygonAmoy.title",
6720
+ copyKey: "settings.wallet.step.polygonAmoy.copy",
6721
+ action: "bootstrap-polygon-amoy",
6722
+ },
6723
+ polygon: {
6724
+ environment: "mainnet",
6725
+ family: "evm",
6726
+ asset: "usdc",
6727
+ titleKey: "settings.wallet.step.polygon.title",
6728
+ copyKey: "settings.wallet.step.polygon.copy",
6729
+ action: "bootstrap-polygon",
6730
+ releaseStatus: "comingSoon",
6731
+ },
6732
+ liquidtestnet: {
6733
+ environment: "testnet",
6734
+ family: "liquid",
6735
+ asset: "usdt",
6736
+ titleKey: "settings.wallet.step.liquidTestnet.title",
6737
+ copyKey: "settings.wallet.step.liquidTestnet.copy",
6738
+ },
6739
+ liquidv1: {
6740
+ environment: "mainnet",
6741
+ family: "liquid",
6742
+ asset: "usdt",
6743
+ titleKey: "settings.wallet.step.liquid.title",
6744
+ copyKey: "settings.wallet.step.liquid.copy",
6745
+ releaseStatus: "comingSoon",
6746
+ },
6747
+ };
6748
+ }
6749
+
6750
+ function paymentCapabilityChainDefinitions() {
6751
+ return {
6752
+ base: {
6753
+ page: "walletInventoryBase",
6754
+ titleKey: "settings.wallet.chain.base.title",
6755
+ copyKey: "settings.wallet.chain.base.copy",
6756
+ networks: ["base-sepolia", "base"],
6757
+ },
6758
+ liquid: {
6759
+ page: "walletInventoryLiquid",
6760
+ titleKey: "settings.wallet.chain.liquid.title",
6761
+ copyKey: "settings.wallet.chain.liquid.copy",
6762
+ networks: ["liquidtestnet", "liquidv1"],
6763
+ },
6764
+ polygon: {
6765
+ page: "walletInventoryPolygon",
6766
+ titleKey: "settings.wallet.chain.polygon.title",
6767
+ copyKey: "settings.wallet.chain.polygon.copy",
6768
+ networks: ["polygon-amoy", "polygon"],
6769
+ },
6770
+ };
6771
+ }
6772
+
6773
+ function paymentCapabilityChainDefinition(chain) {
6774
+ return paymentCapabilityChainDefinitions()[chain] || null;
6775
+ }
6776
+
6777
+ function paymentCapabilityDefinition(network) {
6778
+ return paymentCapabilityDefinitions()[network] || null;
6779
+ }
6780
+
6781
+ function allPaymentCapabilities(hazbase) {
6782
+ return Array.isArray(hazbase?.paymentCapabilities) ? hazbase.paymentCapabilities : [];
6783
+ }
6784
+
6785
+ function isPaymentCapabilityAvailable(network) {
6786
+ return paymentCapabilityDefinition(network)?.releaseStatus !== "comingSoon";
6787
+ }
6788
+
6789
+ function availablePaymentCapabilityNetworks(networks) {
6790
+ return (Array.isArray(networks) ? networks : []).filter((network) => isPaymentCapabilityAvailable(network));
6791
+ }
6792
+
6793
+ function configuredPaymentCapabilities(hazbase) {
6794
+ return allPaymentCapabilities(hazbase).filter((entry) => entry?.configured && entry?.enabled !== false && (entry.payTo || entry.payoutAddress));
6795
+ }
6796
+
6797
+ function agentEligiblePaymentCapabilities(hazbase) {
6798
+ return configuredPaymentCapabilities(hazbase).filter((entry) => isPaymentCapabilityAvailable(entry.network));
6799
+ }
6800
+
6801
+ function configuredPaymentCapabilityNetworkCount(hazbase, networks) {
6802
+ const shouldCountAllNetworks = !Array.isArray(networks);
6803
+ const networkSet = new Set(shouldCountAllNetworks ? [] : networks);
6804
+ const configuredNetworks = new Set();
6805
+ for (const entry of configuredPaymentCapabilities(hazbase)) {
6806
+ if (shouldCountAllNetworks || networkSet.has(entry?.network)) {
6807
+ configuredNetworks.add(entry.network);
6473
6808
  }
6809
+ }
6810
+ return configuredNetworks.size;
6811
+ }
6812
+
6813
+ function paymentCapabilityChainValue(hazbase, chain) {
6814
+ const definition = paymentCapabilityChainDefinition(chain);
6815
+ const networks = definition?.networks || [];
6816
+ const availableNetworks = availablePaymentCapabilityNetworks(networks);
6817
+ const countedNetworks = availableNetworks.length ? availableNetworks : networks;
6818
+ return L("settings.wallet.chain.configuredValue", {
6819
+ count: configuredPaymentCapabilityNetworkCount(hazbase, countedNetworks),
6820
+ total: countedNetworks.length,
6821
+ });
6822
+ }
6823
+
6824
+ function resolveCapability(hazbase, network, asset = "") {
6825
+ const normalizedAsset = String(asset || paymentCapabilityDefinition(network)?.asset || "").toLowerCase();
6826
+ return allPaymentCapabilities(hazbase).find((entry) => (
6827
+ entry?.network === network &&
6828
+ String(entry?.asset || paymentCapabilityDefinition(network)?.asset || "").toLowerCase() === normalizedAsset
6829
+ )) || null;
6830
+ }
6831
+
6832
+ function agentPaymentDefaultRef(value) {
6833
+ const network = String(value?.network || "").trim();
6834
+ if (!network) return null;
6835
+ const asset = String(value?.asset || paymentCapabilityDefinition(network)?.asset || "").trim().toLowerCase();
6836
+ if (!asset) return null;
6837
+ return { network, asset };
6838
+ }
6839
+
6840
+ function agentPaymentDefaultKey(value) {
6841
+ const ref = agentPaymentDefaultRef(value);
6842
+ return ref ? `${ref.network}:${ref.asset}` : "";
6843
+ }
6844
+
6845
+ function normalizeAgentPaymentDefaultRefs(values) {
6846
+ const accepts = [];
6847
+ const seen = new Set();
6848
+ for (const value of Array.isArray(values) ? values : []) {
6849
+ const ref = agentPaymentDefaultRef(value);
6850
+ const key = ref ? `${ref.network}:${ref.asset}` : "";
6851
+ if (!ref || !isPaymentCapabilityAvailable(ref.network) || seen.has(key)) continue;
6852
+ seen.add(key);
6853
+ accepts.push(ref);
6854
+ }
6855
+ return accepts;
6856
+ }
6857
+
6858
+ function activeAgentPaymentDefaults(hazbase, { includeDraft = false } = {}) {
6859
+ return includeDraft && state.hazbaseAgentPaymentDefaultsDraft && typeof state.hazbaseAgentPaymentDefaultsDraft === "object"
6860
+ ? state.hazbaseAgentPaymentDefaultsDraft
6861
+ : hazbase?.agentPaymentDefaults;
6862
+ }
6863
+
6864
+ function effectiveAgentPaymentDefaults(hazbase, { includeDraft = false } = {}) {
6865
+ const activeDefaults = activeAgentPaymentDefaults(hazbase, { includeDraft });
6866
+ const defaults = activeDefaults && typeof activeDefaults === "object"
6867
+ ? activeDefaults
6868
+ : { mode: "configured", effectiveAccepts: [] };
6869
+ if (Array.isArray(defaults.effectiveAccepts)) {
6870
+ return normalizeAgentPaymentDefaultRefs(defaults.effectiveAccepts);
6871
+ }
6872
+ if (String(defaults.mode || "") === "custom") {
6873
+ const configuredKeys = new Set(agentEligiblePaymentCapabilities(hazbase).map(agentPaymentDefaultKey).filter(Boolean));
6874
+ return normalizeAgentPaymentDefaultRefs(defaults.accepts).filter((entry) => configuredKeys.has(agentPaymentDefaultKey(entry)));
6875
+ }
6876
+ return agentEligiblePaymentCapabilities(hazbase).map((entry) => ({
6877
+ network: entry.network,
6878
+ asset: entry.asset || paymentCapabilityDefinition(entry.network)?.asset || "",
6879
+ }));
6880
+ }
6881
+
6882
+ function isAgentPaymentDefaultEnabled(hazbase, capability) {
6883
+ if (!state.hazbaseAgentPaymentDefaultsDraft && typeof capability.agentEnabled === "boolean") {
6884
+ return capability.agentEnabled;
6885
+ }
6886
+ const key = agentPaymentDefaultKey(capability);
6887
+ return effectiveAgentPaymentDefaults(hazbase, { includeDraft: true }).some((entry) => agentPaymentDefaultKey(entry) === key);
6888
+ }
6889
+
6890
+ function paymentCapabilityDisplayLabel(capability) {
6891
+ const def = paymentCapabilityDefinition(capability.network);
6892
+ const networkLabel = capability.label || (def ? L(def.titleKey).replace(/^Issue |^Register /u, "") : capability.network);
6893
+ const asset = String(capability.asset || def?.asset || "").toUpperCase();
6894
+ return asset ? `${networkLabel} ${asset}` : networkLabel;
6895
+ }
6474
6896
 
6475
- const mode = step.status === "complete" ? "compact" : "full";
6476
- rendered.push(renderHazbaseWalletStepCard(step, { mode }));
6897
+ function agentPaymentDefaultDisplayItems(hazbase, defaults) {
6898
+ return defaults.map((entry) => {
6899
+ const capability = resolveCapability(hazbase, entry.network, entry.asset) || entry;
6900
+ return {
6901
+ label: paymentCapabilityDisplayLabel(capability),
6902
+ address: capability.payTo || capability.payoutAddress || capability.smartAccountAddress || "",
6903
+ scheme: capability.scheme || "",
6904
+ network: capability.network || entry.network || "",
6905
+ asset: capability.asset || entry.asset || "",
6906
+ };
6907
+ });
6908
+ }
6909
+
6910
+ function renderAgentPaymentDefaultsSummary(hazbase, defaults) {
6911
+ if (!defaults.length) {
6912
+ return {
6913
+ value: L("common.none"),
6914
+ valueTone: "attention",
6915
+ rawValue: false,
6916
+ stacked: false,
6917
+ valueClassName: "",
6918
+ };
6919
+ }
6920
+ const items = agentPaymentDefaultDisplayItems(hazbase, defaults);
6921
+ if (items.length === 1) {
6922
+ return {
6923
+ value: items[0].label,
6924
+ valueTone: "enabled",
6925
+ rawValue: false,
6926
+ stacked: false,
6927
+ valueClassName: "",
6928
+ };
6477
6929
  }
6478
- return `<div class="wallet-step-list">${rendered.join("")}</div>`;
6930
+ return {
6931
+ value: `
6932
+ <button class="wallet-agent-defaults-summary-button" type="button" data-open-agent-payment-defaults aria-label="${escapeHtml(L("settings.wallet.agent.modalTitle"))}">
6933
+ <span class="wallet-agent-default-pill">${escapeHtml(items[0].label)}</span>
6934
+ <span class="wallet-agent-default-more">+${items.length - 1}</span>
6935
+ </button>
6936
+ `,
6937
+ valueTone: "",
6938
+ rawValue: true,
6939
+ stacked: false,
6940
+ valueClassName: "wallet-agent-defaults-summary-wrap",
6941
+ };
6942
+ }
6943
+
6944
+ function shortAddress(value) {
6945
+ const text = String(value || "");
6946
+ if (text.length <= 20) return text;
6947
+ return `${text.slice(0, 10)}...${text.slice(-6)}`;
6479
6948
  }
6480
6949
 
6481
- function renderHazbaseWalletMainnetOptIn() {
6950
+ function renderAgentPaymentDefaultChoice(hazbase, capability) {
6951
+ const checked = isAgentPaymentDefaultEnabled(hazbase, capability);
6952
+ const address = capability.payTo || capability.payoutAddress || "";
6482
6953
  return `
6483
- <button class="wallet-mainnet-optin" type="button" data-hazbase-action="mainnet-opt-in">
6484
- <span class="wallet-mainnet-optin__body">
6485
- <span class="wallet-mainnet-optin__label">${escapeHtml(L("settings.wallet.mainnet.optIn"))}</span>
6486
- <span class="wallet-mainnet-optin__hint muted">${escapeHtml(L("settings.wallet.mainnet.optInHint"))}</span>
6954
+ <label class="settings-choice-row settings-choice-row--checkbox">
6955
+ <span class="settings-row__body">
6956
+ <span class="settings-row__title">${escapeHtml(paymentCapabilityDisplayLabel(capability))}</span>
6957
+ <span class="settings-row__subtitle">${escapeHtml(capability.scheme || "")}${address ? ` · ${escapeHtml(shortAddress(address))}` : ""}</span>
6487
6958
  </span>
6488
- <span class="wallet-mainnet-optin__chevron" aria-hidden="true">→</span>
6489
- </button>
6959
+ <span class="settings-choice-row__check">
6960
+ <input
6961
+ class="settings-choice-row__input"
6962
+ type="checkbox"
6963
+ data-agent-payment-default
6964
+ data-payment-network="${escapeHtml(capability.network)}"
6965
+ data-payment-asset="${escapeHtml(capability.asset || paymentCapabilityDefinition(capability.network)?.asset || "")}"
6966
+ ${checked ? "checked" : ""}
6967
+ />
6968
+ </span>
6969
+ </label>
6490
6970
  `;
6491
6971
  }
6492
6972
 
6973
+ function hazbaseActionPendingKey(action, network = "") {
6974
+ return action === "save-liquid-capability" ? `${action}:${network || ""}` : action;
6975
+ }
6976
+
6977
+ function isHazbaseActionPending(action, network = "") {
6978
+ return state.hazbasePendingAction === hazbaseActionPendingKey(action, network);
6979
+ }
6980
+
6981
+ function renderWalletInventoryCapabilityCard(hazbase, network) {
6982
+ const def = paymentCapabilityDefinition(network);
6983
+ const available = isPaymentCapabilityAvailable(network);
6984
+ const capability = resolveCapability(hazbase, network, def?.asset) || {
6985
+ network,
6986
+ asset: def?.asset || "",
6987
+ family: def?.family || "",
6988
+ configured: false,
6989
+ };
6990
+ const sessionInvalid = Boolean(hazbase.sessionInvalid);
6991
+ const signedIn = Boolean(hazbase.signedIn) && !sessionInvalid;
6992
+ const hasPasskey = Boolean(hazbase.passkeyRegistered || hazbase.credentialId || hazbase.deviceBindingId);
6993
+ const configured = Boolean(capability.payTo || capability.payoutAddress || capability.smartAccountAddress);
6994
+ const needsPasskey = def?.family === "evm";
6995
+ const canConfigure = signedIn && (!needsPasskey || hasPasskey);
6996
+ const actionBlocked = Boolean(state.hazbasePendingAction);
6997
+ const environmentKey = def?.environment === "mainnet"
6998
+ ? "settings.wallet.inventory.environment.mainnet"
6999
+ : "settings.wallet.inventory.environment.testnet";
7000
+ const actions = [];
7001
+ if (!available) {
7002
+ actions.push(`
7003
+ <button class="secondary secondary--wide" type="button" disabled>${escapeHtml(L("settings.wallet.status.comingSoon"))}</button>
7004
+ `);
7005
+ } else if (!configured && def?.family === "evm") {
7006
+ const pending = isHazbaseActionPending(def.action);
7007
+ const labelKey = {
7008
+ base: "settings.hazbase.action.bootstrapBase",
7009
+ "base-sepolia": "settings.hazbase.action.bootstrapBaseSepolia",
7010
+ polygon: "settings.hazbase.action.bootstrapPolygon",
7011
+ "polygon-amoy": "settings.hazbase.action.bootstrapPolygonAmoy",
7012
+ }[network] || "settings.hazbase.action.bootstrapBaseSepolia";
7013
+ actions.push(`
7014
+ <button
7015
+ class="${canConfigure ? "primary" : "secondary"} ${canConfigure ? "primary--wide" : "secondary--wide"}${pending ? " is-loading" : ""}"
7016
+ type="button"
7017
+ data-hazbase-action="${escapeHtml(def.action)}"
7018
+ ${canConfigure && !actionBlocked ? "" : "disabled"}
7019
+ ${pending ? 'aria-busy="true"' : ""}
7020
+ >${escapeHtml(L(labelKey))}</button>
7021
+ `);
7022
+ } else if (!configured && def?.family === "liquid") {
7023
+ const pending = isHazbaseActionPending("save-liquid-capability", network);
7024
+ actions.push(`
7025
+ <button
7026
+ class="${canConfigure ? "primary" : "secondary"} ${canConfigure ? "primary--wide" : "secondary--wide"}${pending ? " is-loading" : ""}"
7027
+ type="button"
7028
+ data-hazbase-action="save-liquid-capability"
7029
+ data-payment-network="${escapeHtml(network)}"
7030
+ ${canConfigure && !actionBlocked ? "" : "disabled"}
7031
+ ${pending ? 'aria-busy="true"' : ""}
7032
+ >${escapeHtml(L("settings.hazbase.action.saveLiquidCapability"))}</button>
7033
+ `);
7034
+ }
7035
+ return renderHazbaseWalletStepCard({
7036
+ number: 0,
7037
+ eyebrow: L(environmentKey),
7038
+ icon: "coin",
7039
+ title: L(def.titleKey),
7040
+ copy: L(def.copyKey),
7041
+ detail: !available
7042
+ ? L("settings.wallet.mainnet.comingSoonDetail")
7043
+ : configured ? (capability.payTo || capability.payoutAddress || capability.smartAccountAddress) : L("settings.hazbase.wallet.missing"),
7044
+ monoDetail: available && configured,
7045
+ status: !available ? "comingSoon" : configured ? "complete" : canConfigure ? "optional" : "locked",
7046
+ form: available && !configured && def?.family === "liquid" ? renderLiquidCapabilityForm(network, hazbase) : "",
7047
+ actions,
7048
+ });
7049
+ }
7050
+
6493
7051
  function deriveHazbaseWalletFlow(hazbase) {
6494
- const accounts = Array.isArray(hazbase.accounts) ? hazbase.accounts : [];
6495
- const baseSepolia = accounts.find((entry) => Number(entry.chainId) === 84532) || null;
6496
- const baseMainnet = accounts.find((entry) => Number(entry.chainId) === 8453) || null;
6497
7052
  const sessionInvalid = Boolean(hazbase.sessionInvalid);
6498
7053
  const signedIn = Boolean(hazbase.signedIn) && !sessionInvalid;
6499
7054
  const passkeyHost = hazbasePasskeyHostSupport();
6500
- const hasPasskey = Boolean(hazbase.credentialId || hazbase.deviceBindingId);
6501
- const hasBaseSepolia = Boolean(baseSepolia?.smartAccountAddress);
6502
- const hasBaseMainnet = Boolean(baseMainnet?.smartAccountAddress);
6503
- const coreReady = signedIn && hasPasskey && hasBaseSepolia;
7055
+ const hasPasskey = Boolean(hazbase.passkeyRegistered || hazbase.credentialId || hazbase.deviceBindingId);
6504
7056
 
6505
- const actionButton = (labelKey, action, { primary = false, disabled = false } = {}) => `
7057
+ const actionButton = (labelKey, action, { primary = false, disabled = false, attrs = "" } = {}) => {
7058
+ const pending = isHazbaseActionPending(action);
7059
+ const blocked = disabled || Boolean(state.hazbasePendingAction);
7060
+ return `
6506
7061
  <button
6507
- class="${primary ? "primary" : "secondary"} ${primary ? "primary--wide" : "secondary--wide"}"
7062
+ class="${primary ? "primary" : "secondary"} ${primary ? "primary--wide" : "secondary--wide"}${pending ? " is-loading" : ""}"
6508
7063
  type="button"
6509
7064
  data-hazbase-action="${action}"
6510
- ${disabled ? "disabled" : ""}
7065
+ ${attrs}
7066
+ ${blocked ? "disabled" : ""}
7067
+ ${pending ? 'aria-busy="true"' : ""}
6511
7068
  >${escapeHtml(L(labelKey))}</button>
6512
7069
  `;
7070
+ };
6513
7071
 
6514
7072
  return {
6515
7073
  hasPasskey,
6516
- baseSepolia,
6517
- baseMainnet,
6518
7074
  sessionInvalid,
6519
- coreReady,
6520
7075
  steps: [
6521
7076
  {
7077
+ kind: "auth",
6522
7078
  number: 1,
6523
7079
  icon: "approval",
6524
7080
  title: sessionInvalid ? L("settings.wallet.step.refreshSession.title") : L("settings.wallet.step.signIn.title"),
@@ -6560,6 +7116,7 @@ function deriveHazbaseWalletFlow(hazbase) {
6560
7116
  ],
6561
7117
  },
6562
7118
  {
7119
+ kind: "auth",
6563
7120
  number: 2,
6564
7121
  icon: "lock",
6565
7122
  title: L("settings.wallet.step.passkey.title"),
@@ -6579,79 +7136,31 @@ function deriveHazbaseWalletFlow(hazbase) {
6579
7136
  }),
6580
7137
  ],
6581
7138
  },
6582
- {
6583
- number: 3,
6584
- icon: "coin",
6585
- title: L("settings.wallet.step.baseSepolia.title"),
6586
- copy: L("settings.wallet.step.baseSepolia.copy"),
6587
- detail: baseSepolia?.smartAccountAddress || L("settings.hazbase.wallet.missing"),
6588
- monoDetail: Boolean(baseSepolia?.smartAccountAddress),
6589
- status: hasBaseSepolia ? "complete" : signedIn && hasPasskey ? "current" : "locked",
6590
- actions: hasBaseSepolia
6591
- ? []
6592
- : [
6593
- actionButton("settings.hazbase.action.bootstrapBaseSepolia", "bootstrap-base-sepolia", {
6594
- primary: signedIn && hasPasskey,
6595
- disabled: !signedIn || !hasPasskey,
6596
- }),
6597
- ],
6598
- },
6599
- {
6600
- number: 4,
6601
- icon: "coin",
6602
- title: L("settings.wallet.step.base.title"),
6603
- copy: L("settings.wallet.step.base.copy"),
6604
- detail: baseMainnet?.smartAccountAddress || L("settings.wallet.step.base.comingSoonDetail"),
6605
- monoDetail: Boolean(baseMainnet?.smartAccountAddress),
6606
- status: hasBaseMainnet ? "complete" : "comingSoon",
6607
- actions: [],
6608
- },
6609
7139
  ],
6610
7140
  };
6611
7141
  }
6612
7142
 
6613
- function renderHazbaseWalletBanner(flow) {
6614
- const title = flow.sessionInvalid
6615
- ? L("settings.wallet.sessionExpired.title")
6616
- : flow.coreReady ? L("settings.wallet.ready.title") : L("settings.wallet.flow.banner.title");
6617
- const className = flow.coreReady
6618
- ? "settings-copy-block settings-copy-block--stacked wallet-flow-banner wallet-flow-banner--ready"
6619
- : flow.sessionInvalid
6620
- ? "settings-copy-block settings-copy-block--stacked wallet-flow-banner wallet-flow-banner--session-expired"
6621
- : "settings-copy-block settings-copy-block--stacked wallet-flow-banner";
6622
- // Ready state: surface the smart-account address prominently so the user
6623
- // can see at a glance which wallet agents will use as `--pay-to` when
6624
- // gating paid shares / A2A payouts. The address is the single most
6625
- // actionable fact on this page once setup is complete — muted filler
6626
- // copy buries it.
6627
- const payoutAddress = flow.baseSepolia?.smartAccountAddress || "";
6628
- const copyLabel = L("settings.wallet.ready.copyAddress");
6629
- const body = flow.coreReady && payoutAddress
6630
- ? `
6631
- <p class="wallet-flow-banner__copy muted">${escapeHtml(L("settings.wallet.ready.payoutIntro"))}</p>
6632
- <button
6633
- class="wallet-flow-banner__address"
6634
- type="button"
6635
- data-wallet-address-copy="${escapeHtml(payoutAddress)}"
6636
- aria-label="${escapeHtml(copyLabel)}: ${escapeHtml(payoutAddress)}"
6637
- >
6638
- <span class="wallet-flow-banner__address-text">${escapeHtml(payoutAddress)}</span>
6639
- <span class="wallet-flow-banner__address-icon-slot">
6640
- <span class="wallet-flow-banner__address-icon wallet-flow-banner__address-icon--copy" aria-hidden="true">${renderIcon("copy")}</span>
6641
- <span class="wallet-flow-banner__address-icon wallet-flow-banner__address-icon--check" aria-hidden="true">${renderIcon("check")}</span>
6642
- </span>
6643
- </button>
6644
- `
6645
- : `<p class="wallet-flow-banner__copy muted">${escapeHtml(
6646
- flow.sessionInvalid
6647
- ? L("settings.wallet.sessionExpired.copy")
6648
- : flow.coreReady ? L("settings.wallet.ready.copy") : L("settings.wallet.flow.copy"),
6649
- )}</p>`;
7143
+ function renderLiquidCapabilityForm(network, hazbase) {
7144
+ const capabilities = Array.isArray(hazbase.paymentCapabilities) ? hazbase.paymentCapabilities : [];
7145
+ const existing = capabilities.find((entry) => entry.network === network) || {};
7146
+ const placeholder = network === "liquidtestnet" ? "tlq1..." : "lq1...";
7147
+ const error = hazbaseFormError("liquid", network);
7148
+ const errorId = `hazbase-liquid-error-${network}`;
6650
7149
  return `
6651
- <div class="${className}">
6652
- <p class="wallet-flow-banner__eyebrow">${escapeHtml(L("settings.hazbase.title"))}</p>
6653
- <p class="wallet-flow-banner__title">${escapeHtml(title)}</p>
6654
- ${body}
7150
+ <div class="wallet-step-card__form">
7151
+ <label class="wallet-step-card__field">
7152
+ <span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.liquidAddressLabel"))}</span>
7153
+ <input
7154
+ class="wallet-step-card__field-input wallet-step-card__field-input--mono${error ? " wallet-step-card__field-input--error" : ""}"
7155
+ data-hazbase-input="liquid-payto"
7156
+ data-payment-network="${escapeHtml(network)}"
7157
+ value="${escapeHtml(existing.payTo || "")}"
7158
+ placeholder="${escapeHtml(placeholder)}"
7159
+ autocomplete="off"
7160
+ ${error ? `aria-invalid="true" aria-describedby="${escapeHtml(errorId)}"` : ""}
7161
+ />
7162
+ ${error ? `<span class="wallet-step-card__field-error" id="${escapeHtml(errorId)}">${escapeHtml(error)}</span>` : ""}
7163
+ </label>
6655
7164
  </div>
6656
7165
  `;
6657
7166
  }
@@ -6705,7 +7214,7 @@ function renderHazbaseWalletStepCard(step, { mode = "full" } = {}) {
6705
7214
  <div class="wallet-step-card__headline">
6706
7215
  <span class="wallet-step-card__icon" aria-hidden="true">${renderIcon(step.icon)}</span>
6707
7216
  <div class="wallet-step-card__title-wrap">
6708
- <p class="wallet-step-card__eyebrow">${escapeHtml(L("settings.wallet.stepNumber", { count: step.number }))}</p>
7217
+ <p class="wallet-step-card__eyebrow">${escapeHtml(step.eyebrow || L("settings.wallet.stepNumber", { count: step.number }))}</p>
6709
7218
  <h3 class="wallet-step-card__title">${escapeHtml(step.title)}</h3>
6710
7219
  </div>
6711
7220
  </div>
@@ -6730,6 +7239,10 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6730
7239
  // as the intentional escape hatch — clicking it reverts the form to the
6731
7240
  // pre-sent state (code discarded, email stays for easy typo recovery).
6732
7241
  const emailLocked = Boolean(otpRequested);
7242
+ const emailError = hazbaseFormError("email");
7243
+ const otpError = hazbaseFormError("otp");
7244
+ const emailErrorId = "hazbase-email-error";
7245
+ const otpErrorId = "hazbase-otp-error";
6733
7246
  const emailLabelRow = emailLocked
6734
7247
  ? `<span class="wallet-step-card__field-label-row">
6735
7248
  <span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.emailLabel"))}</span>
@@ -6737,6 +7250,7 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6737
7250
  type="button"
6738
7251
  class="wallet-step-card__field-link"
6739
7252
  data-hazbase-action="change-email"
7253
+ ${state.hazbasePendingAction ? "disabled" : ""}
6740
7254
  >${escapeHtml(L("settings.hazbase.action.changeEmail"))}</button>
6741
7255
  </span>`
6742
7256
  : `<span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.emailLabel"))}</span>`;
@@ -6745,7 +7259,7 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6745
7259
  ${emailLabelRow}
6746
7260
  <input
6747
7261
  type="email"
6748
- class="wallet-step-card__field-input${emailLocked ? " wallet-step-card__field-input--locked" : ""}"
7262
+ class="wallet-step-card__field-input${emailLocked ? " wallet-step-card__field-input--locked" : ""}${emailError ? " wallet-step-card__field-input--error" : ""}"
6749
7263
  data-hazbase-input="otp-email"
6750
7264
  value="${escapeHtml(email || "")}"
6751
7265
  placeholder="${escapeHtml(L("settings.hazbase.field.emailPlaceholder"))}"
@@ -6755,7 +7269,9 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6755
7269
  autocorrect="off"
6756
7270
  spellcheck="false"
6757
7271
  ${emailLocked ? "disabled aria-disabled=\"true\"" : ""}
7272
+ ${emailError ? `aria-invalid="true" aria-describedby="${escapeHtml(emailErrorId)}"` : ""}
6758
7273
  />
7274
+ ${emailError ? `<span class="wallet-step-card__field-error" id="${escapeHtml(emailErrorId)}">${escapeHtml(emailError)}</span>` : ""}
6759
7275
  </label>
6760
7276
  `;
6761
7277
  const otpField = otpRequested
@@ -6764,7 +7280,7 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6764
7280
  <span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.otpLabel"))}</span>
6765
7281
  <input
6766
7282
  type="text"
6767
- class="wallet-step-card__field-input wallet-step-card__field-input--mono"
7283
+ class="wallet-step-card__field-input wallet-step-card__field-input--mono${otpError ? " wallet-step-card__field-input--error" : ""}"
6768
7284
  data-hazbase-input="otp-code"
6769
7285
  value="${escapeHtml(code || "")}"
6770
7286
  placeholder="${escapeHtml(L("settings.hazbase.field.otpPlaceholder"))}"
@@ -6774,7 +7290,9 @@ function renderHazbaseSignInForm({ email, otpRequested, code }) {
6774
7290
  autocorrect="off"
6775
7291
  spellcheck="false"
6776
7292
  maxlength="12"
7293
+ ${otpError ? `aria-invalid="true" aria-describedby="${escapeHtml(otpErrorId)}"` : ""}
6777
7294
  />
7295
+ ${otpError ? `<span class="wallet-step-card__field-error" id="${escapeHtml(otpErrorId)}">${escapeHtml(otpError)}</span>` : ""}
6778
7296
  </label>
6779
7297
  `
6780
7298
  : "";
@@ -6822,11 +7340,10 @@ function formatExpiresIn(ms) {
6822
7340
  return rtf ? rtf.format(hr, "hour") : `in ${hr}h`;
6823
7341
  }
6824
7342
 
6825
- // USDC has 6 decimals, stored as atomic units in a string (e.g. "100000" ⇒
6826
- // $0.10). Mirrors formatUsdc() in scripts/share-cli.mjs — kept as a small
7343
+ // Payment amounts are stored as asset-native atomic units. Kept as a small
6827
7344
  // standalone helper rather than a shared module since the app bundle has no
6828
7345
  // build step that pulls from scripts/.
6829
- function formatUsdcAtomic(atomic) {
7346
+ function formatAtomicAmount(atomic, decimals = 6) {
6830
7347
  let n;
6831
7348
  try {
6832
7349
  n = BigInt(String(atomic ?? "0"));
@@ -6834,12 +7351,33 @@ function formatUsdcAtomic(atomic) {
6834
7351
  return "0.00";
6835
7352
  }
6836
7353
  if (n < 0n) n = -n;
6837
- const whole = n / 1_000_000n;
6838
- const frac = (n % 1_000_000n).toString().padStart(6, "0").replace(/0+$/u, "");
7354
+ const places = Math.max(0, Math.min(18, Number(decimals) || 0));
7355
+ const scale = 10n ** BigInt(places);
7356
+ const whole = scale > 0n ? n / scale : n;
7357
+ const frac = scale > 0n ? (n % scale).toString().padStart(places, "0").replace(/0+$/u, "") : "";
6839
7358
  if (!frac) return `${whole}.00`;
6840
7359
  return `${whole}.${frac.padEnd(2, "0")}`;
6841
7360
  }
6842
7361
 
7362
+ function formatUsdcAtomic(atomic) {
7363
+ return formatAtomicAmount(atomic, 6);
7364
+ }
7365
+
7366
+ function paymentAssetDecimals(asset) {
7367
+ const key = String(asset || "").toLowerCase();
7368
+ if (key === "jpyc") return 18;
7369
+ if (key === "usdt" || key === "lbtc") return 8;
7370
+ return 6;
7371
+ }
7372
+
7373
+ function paymentAssetLabel(asset) {
7374
+ const key = String(asset || "").toLowerCase();
7375
+ if (key === "jpyc") return "JPYC";
7376
+ if (key === "usdt") return "USDt";
7377
+ if (key === "lbtc") return "L-BTC";
7378
+ return "USDC";
7379
+ }
7380
+
6843
7381
  function renderSettingsInfoRow(label, value, options = {}) {
6844
7382
  const tone = settingsNavValueTone(value, options.valueTone || "");
6845
7383
  const rowClassName = [
@@ -8461,6 +8999,36 @@ function renderHazbaseLogoutConfirmModal() {
8461
8999
  `;
8462
9000
  }
8463
9001
 
9002
+ function renderAgentPaymentDefaultsModal() {
9003
+ if (!state.agentPaymentDefaultsModalOpen) {
9004
+ return "";
9005
+ }
9006
+ const hazbase = state.hazbaseStatus || {};
9007
+ const items = agentPaymentDefaultDisplayItems(hazbase, effectiveAgentPaymentDefaults(hazbase));
9008
+ if (!items.length) {
9009
+ return "";
9010
+ }
9011
+ return `
9012
+ <div class="modal-backdrop" data-close-agent-payment-defaults>
9013
+ <section class="modal-card modal-card--wallet-defaults" role="dialog" aria-modal="true" aria-labelledby="agent-payment-defaults-title">
9014
+ <div class="helper-copy">
9015
+ <strong id="agent-payment-defaults-title">${escapeHtml(L("settings.wallet.agent.modalTitle"))}</strong>
9016
+ <p class="muted">${escapeHtml(L("settings.wallet.agent.modalCopy"))}</p>
9017
+ </div>
9018
+ <div class="wallet-agent-defaults-modal-list">
9019
+ ${items.map((item) => `
9020
+ <div class="wallet-agent-defaults-modal-row">
9021
+ <span class="wallet-agent-defaults-modal-title">${escapeHtml(item.label)}</span>
9022
+ <span class="wallet-agent-defaults-modal-meta">${escapeHtml([item.scheme, shortAddress(item.address)].filter(Boolean).join(" · "))}</span>
9023
+ </div>
9024
+ `).join("")}
9025
+ </div>
9026
+ <button class="primary primary--wide" type="button" data-close-agent-payment-defaults>${escapeHtml(L("common.close"))}</button>
9027
+ </section>
9028
+ </div>
9029
+ `;
9030
+ }
9031
+
8464
9032
  function renderLogoutConfirmModal() {
8465
9033
  if (!state.logoutConfirmOpen || !state.session?.authenticated) {
8466
9034
  return "";
@@ -9493,9 +10061,48 @@ function bindShellInteractions() {
9493
10061
 
9494
10062
  for (const button of document.querySelectorAll("[data-hazbase-action]")) {
9495
10063
  button.addEventListener("click", async () => {
10064
+ const action = button.dataset.hazbaseAction || "";
10065
+ if (!action || button.disabled || state.hazbasePendingAction) {
10066
+ return;
10067
+ }
10068
+ const actionNetwork = button.dataset.paymentNetwork || "";
9496
10069
  state.hazbaseNotice = "";
9497
10070
  state.hazbaseError = "";
9498
- const action = button.dataset.hazbaseAction || "";
10071
+ clearHazbaseFormErrorsForAction(action, actionNetwork);
10072
+ if (action === "logout") {
10073
+ // Gate wallet logout behind an explicit confirm — the modal's
10074
+ // "confirm" button dispatches `logout-confirm`, which actually
10075
+ // hits the API. Short-circuit here so the initial click only
10076
+ // opens the dialog.
10077
+ state.hazbaseLogoutConfirmOpen = true;
10078
+ await renderShell();
10079
+ return;
10080
+ }
10081
+ if (action === "change-email") {
10082
+ // Flip the form back to pre-send mode. We keep the email so typo
10083
+ // recovery ("hoshin" -> "hoshino") stays one edit away, but drop
10084
+ // the now-stale OTP code. No server call — hazbase invalidates
10085
+ // the previous OTP automatically when a fresh one is requested.
10086
+ state.hazbaseOtpRequested = false;
10087
+ state.hazbaseOtpCode = "";
10088
+ state.hazbaseNotice = "";
10089
+ state.hazbaseError = "";
10090
+ clearHazbaseFormError("email");
10091
+ clearHazbaseFormError("otp");
10092
+ await renderShell();
10093
+ // Move focus back to the (now re-enabled) email input so the user
10094
+ // can start editing immediately.
10095
+ document.querySelector('[data-hazbase-input="otp-email"]')?.focus();
10096
+ return;
10097
+ }
10098
+ state.hazbasePendingAction = hazbaseActionPendingKey(action, actionNetwork);
10099
+ // Wallet auth touches external services and WebAuthn; without an
10100
+ // immediate affordance the button looked dead while the request was
10101
+ // still in flight. Mutate the current DOM synchronously so the first
10102
+ // tap visibly "takes", then let the final render reconcile state.
10103
+ button.classList.add("is-loading");
10104
+ button.disabled = true;
10105
+ button.setAttribute("aria-busy", "true");
9499
10106
  try {
9500
10107
  if (action === "request-otp") {
9501
10108
  // Read from the DOM input, not just state. The state mirror is
@@ -9505,14 +10112,20 @@ for (const button of document.querySelectorAll("[data-hazbase-action]")) {
9505
10112
  // Treat DOM as authoritative and fall back to state.
9506
10113
  const emailInput = document.querySelector('[data-hazbase-input="otp-email"]');
9507
10114
  const email = (emailInput?.value || state.hazbaseOtpEmail || "").trim();
9508
- if (!email) throw new Error(L("error.hazbaseEmailRequired"));
9509
- const result = await apiPost("/api/hazbase/request-otp", { email });
10115
+ if (!email) {
10116
+ setHazbaseFormError("email", L("error.hazbaseEmailRequired"));
10117
+ emailInput?.focus();
10118
+ return;
10119
+ }
10120
+ const result = await apiPost("/api/hazbase/request-otp", { email }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9510
10121
  state.hazbaseOtpEmail = email;
9511
10122
  state.hazbaseOtpRequested = true;
9512
10123
  state.hazbaseOtpCode = "";
9513
- state.hazbaseNotice = result?.debugCode
10124
+ clearHazbaseFormError("email");
10125
+ clearHazbaseFormError("otp");
10126
+ showToast(result?.debugCode
9514
10127
  ? `${L("settings.hazbase.notice.otpRequested")} (${result.debugCode})`
9515
- : L("settings.hazbase.notice.otpRequested");
10128
+ : L("settings.hazbase.notice.otpRequested"));
9516
10129
  } else if (action === "verify-otp") {
9517
10130
  // Same pattern — DOM wins, state fallback covers the rare case
9518
10131
  // where the field was removed/re-added between type and click.
@@ -9520,88 +10133,156 @@ for (const button of document.querySelectorAll("[data-hazbase-action]")) {
9520
10133
  const codeInput = document.querySelector('[data-hazbase-input="otp-code"]');
9521
10134
  const email = (emailInput?.value || state.hazbaseOtpEmail || "").trim();
9522
10135
  const code = (codeInput?.value || state.hazbaseOtpCode || "").trim();
9523
- if (!email) throw new Error(L("error.hazbaseEmailRequired"));
9524
- if (!code) throw new Error(L("error.hazbaseOtpRequired"));
9525
- await apiPost("/api/hazbase/verify-otp", { email, code });
10136
+ let hasInlineError = false;
10137
+ if (!email) {
10138
+ setHazbaseFormError("email", L("error.hazbaseEmailRequired"));
10139
+ hasInlineError = true;
10140
+ }
10141
+ if (!code) {
10142
+ setHazbaseFormError("otp", L("error.hazbaseOtpRequired"));
10143
+ hasInlineError = true;
10144
+ }
10145
+ if (hasInlineError) {
10146
+ (email ? codeInput : emailInput)?.focus();
10147
+ return;
10148
+ }
10149
+ await apiPost("/api/hazbase/verify-otp", { email, code }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9526
10150
  state.hazbaseOtpRequested = false;
9527
10151
  state.hazbaseOtpEmail = "";
9528
10152
  state.hazbaseOtpCode = "";
9529
- state.hazbaseNotice = L("settings.hazbase.notice.otpVerified");
10153
+ clearHazbaseFormError("email");
10154
+ clearHazbaseFormError("otp");
10155
+ showToast(L("settings.hazbase.notice.otpVerified"));
9530
10156
  } else if (action === "register-passkey") {
9531
10157
  if (!hazbasePasskeyHostSupport().eligible) {
9532
10158
  throw new Error(L("error.hazbasePasskeyLocalHostRequired"));
9533
10159
  }
9534
10160
  const { createPasskeyRegistrationCredential } = await loadHazbasePasskeyModule();
9535
- const challenge = await apiPost("/api/hazbase/passkey/register/challenge", {});
9536
- const credential = await createPasskeyRegistrationCredential(challenge);
10161
+ const challenge = await apiPost("/api/hazbase/passkey/register/challenge", {}, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10162
+ let credential;
10163
+ try {
10164
+ credential = await createPasskeyRegistrationCredential(challenge);
10165
+ } catch (error) {
10166
+ if (error?.name === "InvalidStateError" || /invalid state/i.test(error?.message || String(error))) {
10167
+ throw new Error(L("error.hazbasePasskeyAlreadyRegistered"));
10168
+ }
10169
+ throw error;
10170
+ }
9537
10171
  await apiPost("/api/hazbase/passkey/register/complete", {
9538
10172
  challengeId: challenge.challengeId,
9539
10173
  credential,
9540
- });
9541
- state.hazbaseNotice = L("settings.hazbase.notice.passkeyRegistered");
9542
- } else if (action === "bootstrap-base-sepolia" || action === "bootstrap-base") {
10174
+ }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10175
+ showToast(L("settings.hazbase.notice.passkeyRegistered"));
10176
+ } else if (action === "bootstrap-base-sepolia" || action === "bootstrap-base" || action === "bootstrap-polygon-amoy" || action === "bootstrap-polygon") {
9543
10177
  if (!hazbasePasskeyHostSupport().eligible) {
9544
10178
  throw new Error(L("error.hazbasePasskeyLocalHostRequired"));
9545
10179
  }
9546
10180
  const { createPasskeyAssertionCredential } = await loadHazbasePasskeyModule();
9547
- const chainId = action === "bootstrap-base" ? 8453 : 84532;
9548
- const challenge = await apiPost("/api/hazbase/passkey/assert/challenge", { purpose: "bootstrap" });
10181
+ const chainId = {
10182
+ "bootstrap-base": 8453,
10183
+ "bootstrap-base-sepolia": 84532,
10184
+ "bootstrap-polygon": 137,
10185
+ "bootstrap-polygon-amoy": 80002,
10186
+ }[action];
10187
+ const challenge = await apiPost("/api/hazbase/passkey/assert/challenge", { purpose: "bootstrap" }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9549
10188
  const credential = await createPasskeyAssertionCredential(challenge);
9550
10189
  await apiPost("/api/hazbase/passkey/assert/complete", {
9551
10190
  challengeId: challenge.challengeId,
9552
10191
  credential,
9553
10192
  purpose: "bootstrap",
9554
- });
9555
- await apiPost("/api/hazbase/account/bootstrap", { chainId });
9556
- state.hazbaseNotice = L("settings.hazbase.notice.walletBootstrapped", { chainId });
9557
- } else if (action === "logout") {
9558
- // Gate wallet logout behind an explicit confirm — the modal's
9559
- // "confirm" button dispatches `logout-confirm`, which actually
9560
- // hits the API. Short-circuit here so the initial click only
9561
- // opens the dialog.
9562
- state.hazbaseLogoutConfirmOpen = true;
9563
- await renderShell();
9564
- return;
10193
+ }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10194
+ await apiPost("/api/hazbase/account/bootstrap", { chainId }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10195
+ showToast(L("settings.hazbase.notice.walletBootstrapped", { chainId }));
10196
+ } else if (action === "save-liquid-capability") {
10197
+ const network = actionNetwork;
10198
+ const input = document.querySelector(`[data-hazbase-input="liquid-payto"][data-payment-network="${CSS.escape(network)}"]`);
10199
+ const payTo = (input?.value || "").trim();
10200
+ if (!payTo) {
10201
+ setHazbaseFormError("liquid", L("error.hazbaseLiquidAddressRequired"), network);
10202
+ input?.focus();
10203
+ return;
10204
+ }
10205
+ await apiPost("/api/hazbase/payment-capability", { network, asset: "usdt", payTo }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10206
+ clearHazbaseFormError("liquid", network);
10207
+ showToast(L("settings.hazbase.notice.paymentCapabilitySaved", { network }));
9565
10208
  } else if (action === "logout-confirm") {
9566
10209
  state.hazbaseLogoutConfirmOpen = false;
9567
- await apiPost("/api/hazbase/logout", {});
9568
- state.hazbaseNotice = L("settings.hazbase.notice.signedOut");
10210
+ await apiPost("/api/hazbase/logout", {}, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10211
+ showToast(L("settings.hazbase.notice.signedOut"));
9569
10212
  } else if (action === "refresh-session") {
9570
- await apiPost("/api/hazbase/session/refresh", {});
10213
+ await apiPost("/api/hazbase/session/refresh", {}, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9571
10214
  state.hazbaseOtpRequested = false;
9572
10215
  state.hazbaseOtpCode = "";
9573
- state.hazbaseNotice = L("settings.hazbase.notice.sessionRefreshStarted");
9574
- } else if (action === "change-email") {
9575
- // Flip the form back to pre-send mode. We keep the email so typo
9576
- // recovery ("hoshin" → "hoshino") stays one edit away, but drop
9577
- // the now-stale OTP code. No server call — hazbase invalidates
9578
- // the previous OTP automatically when a fresh one is requested.
9579
- state.hazbaseOtpRequested = false;
9580
- state.hazbaseOtpCode = "";
9581
- state.hazbaseNotice = "";
9582
- state.hazbaseError = "";
9583
- await renderShell();
9584
- // Move focus back to the (now re-enabled) email input so the user
9585
- // can start editing immediately.
9586
- document.querySelector('[data-hazbase-input="otp-email"]')?.focus();
9587
- return;
9588
- } else if (action === "mainnet-opt-in") {
9589
- // Pure client-side reveal — the mainnet step is always in the flow
9590
- // data, we just hide it behind an opt-in link to keep the default
9591
- // path (testnet only) focused. No network call; no status refetch.
9592
- state.hazbaseMainnetOptIn = true;
9593
- await renderShell();
9594
- return;
10216
+ showToast(L("settings.hazbase.notice.sessionRefreshStarted"));
9595
10217
  }
9596
- await fetchHazbaseStatus();
10218
+ await fetchHazbaseStatus({ timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9597
10219
  } catch (error) {
9598
10220
  if (error?.errorKey === "hazbase-session-expired") {
9599
10221
  state.hazbaseOtpRequested = false;
9600
10222
  state.hazbaseOtpCode = "";
9601
- await fetchHazbaseStatus();
10223
+ await fetchHazbaseStatus({ timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
9602
10224
  }
9603
- state.hazbaseError = error.message || String(error);
10225
+ const message = error.message || String(error);
10226
+ if (!applyHazbaseInlineError(action, error, actionNetwork)) {
10227
+ showToast(message, { tone: "error" });
10228
+ }
10229
+ } finally {
10230
+ state.hazbasePendingAction = "";
10231
+ await renderShell();
9604
10232
  }
10233
+ });
10234
+ }
10235
+
10236
+ for (const button of document.querySelectorAll("[data-hazbase-agent-defaults-save], [data-hazbase-agent-defaults-all]")) {
10237
+ button.addEventListener("click", async () => {
10238
+ if (button.disabled || state.hazbasePendingAction) {
10239
+ return;
10240
+ }
10241
+ state.hazbaseNotice = "";
10242
+ state.hazbaseError = "";
10243
+ state.hazbasePendingAction = button.hasAttribute("data-hazbase-agent-defaults-all")
10244
+ ? "agent-defaults-all"
10245
+ : "agent-defaults-save";
10246
+ button.classList.add("is-loading");
10247
+ button.disabled = true;
10248
+ button.setAttribute("aria-busy", "true");
10249
+ try {
10250
+ if (button.hasAttribute("data-hazbase-agent-defaults-all")) {
10251
+ await apiPost("/api/hazbase/agent-payment-defaults", { mode: "configured" }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10252
+ } else {
10253
+ const accepts = state.hazbaseAgentPaymentDefaultsDraft?.mode === "custom"
10254
+ ? normalizeAgentPaymentDefaultRefs(state.hazbaseAgentPaymentDefaultsDraft.accepts)
10255
+ : selectedAgentPaymentDefaultRefs();
10256
+ await apiPost("/api/hazbase/agent-payment-defaults", { mode: "custom", accepts }, { timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10257
+ }
10258
+ state.hazbaseAgentPaymentDefaultsDraft = null;
10259
+ showToast(L("settings.hazbase.notice.agentDefaultsSaved"));
10260
+ await fetchHazbaseStatus({ timeoutMs: HAZBASE_ACTION_TIMEOUT_MS });
10261
+ } catch (error) {
10262
+ showToast(error.message || String(error), { tone: "error" });
10263
+ } finally {
10264
+ state.hazbasePendingAction = "";
10265
+ await renderShell();
10266
+ }
10267
+ });
10268
+ }
10269
+
10270
+ function selectedAgentPaymentDefaultRefs() {
10271
+ return normalizeAgentPaymentDefaultRefs(Array.from(document.querySelectorAll("[data-agent-payment-default]:checked"))
10272
+ .map((input) => ({
10273
+ network: input.dataset.paymentNetwork || "",
10274
+ asset: input.dataset.paymentAsset || "",
10275
+ })));
10276
+ }
10277
+
10278
+ for (const input of document.querySelectorAll("[data-agent-payment-default]")) {
10279
+ input.addEventListener("change", async () => {
10280
+ state.hazbaseNotice = "";
10281
+ state.hazbaseError = "";
10282
+ state.hazbaseAgentPaymentDefaultsDraft = {
10283
+ mode: "custom",
10284
+ accepts: selectedAgentPaymentDefaultRefs(),
10285
+ };
9605
10286
  await renderShell();
9606
10287
  });
9607
10288
  }
@@ -9610,11 +10291,31 @@ for (const button of document.querySelectorAll("[data-hazbase-action]")) {
9610
10291
  // notice clear, etc.) can repopulate `value="..."` without wiping what
9611
10292
  // the user was typing. Reads happen at button-click time against state,
9612
10293
  // which is why we don't need to also query the DOM in the handler.
10294
+ function clearRenderedHazbaseInputError(input) {
10295
+ input.classList.remove("wallet-step-card__field-input--error");
10296
+ input.removeAttribute("aria-invalid");
10297
+ const describedBy = input.getAttribute("aria-describedby");
10298
+ if (describedBy) {
10299
+ document.getElementById(describedBy)?.remove();
10300
+ input.removeAttribute("aria-describedby");
10301
+ }
10302
+ }
10303
+
9613
10304
  for (const input of document.querySelectorAll("[data-hazbase-input]")) {
9614
10305
  const name = input.dataset.hazbaseInput || "";
9615
10306
  input.addEventListener("input", () => {
9616
- if (name === "otp-email") state.hazbaseOtpEmail = input.value;
9617
- else if (name === "otp-code") state.hazbaseOtpCode = input.value;
10307
+ if (name === "otp-email") {
10308
+ state.hazbaseOtpEmail = input.value;
10309
+ clearHazbaseFormError("email");
10310
+ clearRenderedHazbaseInputError(input);
10311
+ } else if (name === "otp-code") {
10312
+ state.hazbaseOtpCode = input.value;
10313
+ clearHazbaseFormError("otp");
10314
+ clearRenderedHazbaseInputError(input);
10315
+ } else if (name === "liquid-payto") {
10316
+ clearHazbaseFormError("liquid", input.dataset.paymentNetwork || "");
10317
+ clearRenderedHazbaseInputError(input);
10318
+ }
9618
10319
  });
9619
10320
  // Enter key submits the step. In the email field it triggers "send"
9620
10321
  // (or "verify" once a code was already issued — the email field stays
@@ -10248,6 +10949,23 @@ function bindSharedUi(renderFn) {
10248
10949
  });
10249
10950
  }
10250
10951
 
10952
+ for (const button of document.querySelectorAll("[data-open-agent-payment-defaults]")) {
10953
+ button.addEventListener("click", async () => {
10954
+ state.agentPaymentDefaultsModalOpen = true;
10955
+ await renderFn();
10956
+ });
10957
+ }
10958
+
10959
+ for (const button of document.querySelectorAll("[data-close-agent-payment-defaults]")) {
10960
+ button.addEventListener("click", async (event) => {
10961
+ if (button.classList.contains("modal-backdrop") && event.target.closest(".modal-card")) {
10962
+ return;
10963
+ }
10964
+ state.agentPaymentDefaultsModalOpen = false;
10965
+ await renderFn();
10966
+ });
10967
+ }
10968
+
10251
10969
  for (const button of document.querySelectorAll("[data-dismiss-install]")) {
10252
10970
  button.addEventListener("click", async () => {
10253
10971
  state.installBannerDismissed = true;
@@ -10406,20 +11124,39 @@ function openSettingsSubpage(page) {
10406
11124
  if (!page) {
10407
11125
  return;
10408
11126
  }
11127
+ const openingFromRoot = !state.settingsSubpage;
10409
11128
  if (!isDesktopLayout()) {
10410
- state.settingsScrollState = {
10411
- y: currentViewportScrollY(),
10412
- };
10413
- state.pendingSettingsScrollRestore = false;
11129
+ if (openingFromRoot) {
11130
+ state.settingsScrollState = {
11131
+ y: currentViewportScrollY(),
11132
+ };
11133
+ state.pendingSettingsScrollRestore = false;
11134
+ }
10414
11135
  state.pendingSettingsSubpageScrollReset = true;
10415
11136
  }
10416
11137
  state.settingsSubpage = page;
10417
11138
  }
10418
11139
 
11140
+ function parentSettingsSubpage(page) {
11141
+ if (page === "walletInventoryBase" || page === "walletInventoryLiquid" || page === "walletInventoryPolygon") {
11142
+ return "walletInventory";
11143
+ }
11144
+ return page === "walletInventory" ? "wallet" : "";
11145
+ }
11146
+
10419
11147
  function closeSettingsSubpage() {
10420
11148
  if (!state.settingsSubpage) {
10421
11149
  return;
10422
11150
  }
11151
+ const parentPage = parentSettingsSubpage(state.settingsSubpage);
11152
+ if (parentPage) {
11153
+ state.settingsSubpage = parentPage;
11154
+ if (!isDesktopLayout()) {
11155
+ state.pendingSettingsSubpageScrollReset = true;
11156
+ state.pendingSettingsScrollRestore = false;
11157
+ }
11158
+ return;
11159
+ }
10423
11160
  state.settingsSubpage = "";
10424
11161
  if (!isDesktopLayout() && state.settingsScrollState) {
10425
11162
  state.pendingSettingsScrollRestore = true;
@@ -11478,7 +12215,7 @@ async function apiPost(url, body, opts = {}) {
11478
12215
  async function readError(response) {
11479
12216
  try {
11480
12217
  const payload = await response.json();
11481
- const errorKey = typeof payload.error === "string" ? payload.error : "";
12218
+ const errorKey = firstApiErrorKey(payload);
11482
12219
  const message = localizeApiError(errorKey || payload.message || response.statusText);
11483
12220
  return { message, errorKey, payload };
11484
12221
  } catch {
@@ -11486,11 +12223,58 @@ async function readError(response) {
11486
12223
  }
11487
12224
  }
11488
12225
 
11489
- function localizeApiError(value) {
12226
+ function firstApiErrorKey(payload) {
12227
+ if (!payload || typeof payload !== "object") {
12228
+ return "";
12229
+ }
12230
+ return [
12231
+ payload.error,
12232
+ payload.code,
12233
+ payload.errorCode,
12234
+ payload.message,
12235
+ ].map(extractApiErrorKey).find(Boolean) || "";
12236
+ }
12237
+
12238
+ function extractApiErrorKey(value) {
11490
12239
  const raw = normalizeClientText(value);
11491
12240
  if (!raw) {
11492
12241
  return "";
11493
12242
  }
12243
+ const embedded = extractEmbeddedApiErrorPayload(raw);
12244
+ if (embedded) {
12245
+ return firstApiErrorKey(embedded);
12246
+ }
12247
+ const [head] = raw.split(":");
12248
+ const normalized = normalizeApiErrorKey(head);
12249
+ return normalized || normalizeApiErrorKey(raw);
12250
+ }
12251
+
12252
+ function extractEmbeddedApiErrorPayload(value) {
12253
+ const start = value.indexOf("{");
12254
+ const end = value.lastIndexOf("}");
12255
+ if (start < 0 || end <= start) {
12256
+ return null;
12257
+ }
12258
+ try {
12259
+ return JSON.parse(value.slice(start, end + 1));
12260
+ } catch {
12261
+ return null;
12262
+ }
12263
+ }
12264
+
12265
+ function normalizeApiErrorKey(value) {
12266
+ return normalizeClientText(value)
12267
+ .toLowerCase()
12268
+ .replace(/_/g, "-")
12269
+ .replace(/[^a-z0-9-]/g, "");
12270
+ }
12271
+
12272
+ function localizeApiError(value) {
12273
+ const original = normalizeClientText(value);
12274
+ const raw = extractApiErrorKey(value) || normalizeApiErrorKey(value);
12275
+ if (!original && !raw) {
12276
+ return "";
12277
+ }
11494
12278
  const map = {
11495
12279
  "pairing-unavailable": "error.pairingUnavailable",
11496
12280
  "invalid-pairing-credentials": "error.invalidPairingCredentials",
@@ -11523,10 +12307,14 @@ function localizeApiError(value) {
11523
12307
  "hazbase-session-expired": "error.hazbaseSessionExpired",
11524
12308
  "hazbase-passkey-local-host-required": "error.hazbasePasskeyLocalHostRequired",
11525
12309
  "hazbase-wallet-account-missing": "error.hazbaseWalletAccountMissing",
12310
+ "email-required": "error.hazbaseEmailRequired",
12311
+ "otp-required": "error.hazbaseOtpRequired",
12312
+ "invalid-liquid-address": "error.hazbaseLiquidAddressInvalid",
12313
+ "payment-network-coming-soon": "error.paymentNetworkComingSoon",
11526
12314
  "unsupported-chain": "error.unsupportedChain",
11527
12315
  };
11528
12316
  const key = map[raw];
11529
- return key ? L(key) : raw;
12317
+ return key ? L(key) : original;
11530
12318
  }
11531
12319
 
11532
12320
  function normalizeClientText(value) {