@sponsorcoin/merit-wallet 0.0.1 → 0.0.2

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 (267) hide show
  1. package/README.md +22 -0
  2. package/dist/AgentSelectDropDown.js +4 -2
  3. package/dist/AgentSelectDropDownWithAvatar.js +2 -1
  4. package/dist/ChangePasswordPanel.js +6 -6
  5. package/dist/ConnectedMessagePanel.js +17 -15
  6. package/dist/ConnectedNetworkPanel.js +7 -12
  7. package/dist/DeleteWalletDialog.js +59 -12
  8. package/dist/DialogPortal.d.ts +3 -0
  9. package/dist/DialogPortal.js +19 -0
  10. package/dist/MeritWallet.d.ts +47 -47
  11. package/dist/MeritWallet.js +1384 -762
  12. package/dist/PasswordPromptDialog.js +8 -6
  13. package/dist/RecoveryPhraseScreen.js +3 -7
  14. package/dist/SecuritySetupScreen.d.ts +44 -0
  15. package/dist/SecuritySetupScreen.js +119 -0
  16. package/dist/SecurityWarningScreen.d.ts +5 -0
  17. package/dist/SecurityWarningScreen.js +18 -0
  18. package/dist/TestAccountsSection.js +2 -1
  19. package/dist/TokenPanel.js +29 -9
  20. package/dist/TokenSlotPanels.js +7 -8
  21. package/dist/UnlockSecondStep.d.ts +7 -0
  22. package/dist/UnlockSecondStep.js +50 -0
  23. package/dist/VaultAccountsPanel.js +22 -22
  24. package/dist/WalletOnboardingPanel.js +11 -12
  25. package/dist/WalletSecuritySection.js +4 -15
  26. package/dist/account/AccountDetailScreen.d.ts +7 -2
  27. package/dist/account/AccountDetailScreen.js +52 -28
  28. package/dist/account/AccountPrivateKeyRow.d.ts +6 -0
  29. package/dist/account/AccountPrivateKeyRow.js +6 -2
  30. package/dist/account/AddAccountFlow.d.ts +18 -18
  31. package/dist/account/AddAccountFlow.js +159 -121
  32. package/dist/account/DetailImageBlock.d.ts +44 -0
  33. package/dist/account/DetailImageBlock.js +46 -0
  34. package/dist/account/DetailScreenLayout.d.ts +38 -0
  35. package/dist/account/DetailScreenLayout.js +30 -0
  36. package/dist/account/MetaDetailScreen.d.ts +30 -0
  37. package/dist/account/MetaDetailScreen.js +33 -0
  38. package/dist/account/RemoveAccountPanel.d.ts +14 -0
  39. package/dist/account/RemoveAccountPanel.js +97 -0
  40. package/dist/account/accountDetailHostFromProfile.d.ts +4 -2
  41. package/dist/account/accountDetailHostFromProfile.js +3 -2
  42. package/dist/account/accountSource.d.ts +36 -0
  43. package/dist/account/accountSource.js +82 -0
  44. package/dist/account/form/AccountAvatarPanel.js +12 -235
  45. package/dist/account/form/AccountFormPanel.d.ts +6 -2
  46. package/dist/account/form/AccountFormPanel.js +82 -114
  47. package/dist/account/form/AvatarControls.d.ts +25 -0
  48. package/dist/account/form/AvatarControls.js +27 -0
  49. package/dist/account/form/AvatarPicture.d.ts +11 -0
  50. package/dist/account/form/AvatarPicture.js +11 -0
  51. package/dist/account/form/CopyIcon.d.ts +1 -0
  52. package/dist/account/form/CopyIcon.js +7 -0
  53. package/dist/account/form/DisconnectedControl.js +2 -1
  54. package/dist/account/form/formTypes.d.ts +0 -1
  55. package/dist/account/form/useAccountForm.d.ts +8 -2
  56. package/dist/account/form/useAccountForm.js +79 -44
  57. package/dist/account/form/useAccountFormDerivedState.js +1 -1
  58. package/dist/account/form/useAccountLiveRefresh.d.ts +15 -0
  59. package/dist/account/form/useAccountLiveRefresh.js +46 -0
  60. package/dist/account/form/useAvatarPreviewSize.d.ts +39 -0
  61. package/dist/account/form/useAvatarPreviewSize.js +245 -0
  62. package/dist/account/responseFailure.d.ts +1 -0
  63. package/dist/account/responseFailure.js +7 -0
  64. package/dist/account/signIn.js +3 -6
  65. package/dist/approval/approvalLifecycle.d.ts +15 -0
  66. package/dist/approval/approvalLifecycle.js +51 -0
  67. package/dist/approval/meritApproval.js +48 -3
  68. package/dist/approval/pendingApprovalMessageStore.d.ts +7 -0
  69. package/dist/approval/pendingApprovalMessageStore.js +18 -0
  70. package/dist/approval/useMeritApprovalMessagePanel.d.ts +7 -0
  71. package/dist/approval/useMeritApprovalMessagePanel.js +132 -0
  72. package/dist/auth/accountNoticeStore.d.ts +22 -0
  73. package/dist/auth/accountNoticeStore.js +31 -0
  74. package/dist/auth/approvalWaiting.d.ts +4 -2
  75. package/dist/auth/approvalWaiting.js +16 -2
  76. package/dist/auth/authenticator.d.ts +29 -4
  77. package/dist/auth/authenticator.js +3 -2
  78. package/dist/auth/connect.js +1 -1
  79. package/dist/auth/injectedAuthenticator.d.ts +4 -1
  80. package/dist/auth/injectedAuthenticator.js +28 -1
  81. package/dist/auth/keptAccounts.d.ts +14 -0
  82. package/dist/auth/keptAccounts.js +126 -0
  83. package/dist/auth/meritConnection.d.ts +31 -0
  84. package/dist/auth/meritConnection.js +65 -0
  85. package/dist/auth/registerApprovingWallet.d.ts +1 -0
  86. package/dist/auth/registerApprovingWallet.js +15 -0
  87. package/dist/auth/useAccountNoticeMessagePanel.d.ts +5 -0
  88. package/dist/auth/useAccountNoticeMessagePanel.js +54 -0
  89. package/dist/auth/useApprovalMessagePanel.js +16 -6
  90. package/dist/auth/walletAuthenticators.d.ts +35 -0
  91. package/dist/auth/walletAuthenticators.js +83 -0
  92. package/dist/auth/walletAuthenticatorsReact.d.ts +9 -0
  93. package/dist/auth/walletAuthenticatorsReact.js +14 -0
  94. package/dist/balance/createBalanceReader.d.ts +23 -0
  95. package/dist/balance/createBalanceReader.js +24 -0
  96. package/dist/chain/chainReconcile.d.ts +68 -0
  97. package/dist/chain/chainReconcile.js +139 -0
  98. package/dist/config/WalletSessionDialogs.js +3 -1
  99. package/dist/debug/DebugBug.d.ts +8 -0
  100. package/dist/debug/DebugBug.js +74 -0
  101. package/dist/debug/debugStore.d.ts +39 -0
  102. package/dist/debug/debugStore.js +148 -0
  103. package/dist/hostContract.d.ts +6 -6
  104. package/dist/index.d.ts +52 -18
  105. package/dist/index.js +46 -13
  106. package/dist/injectableComponentTypes.d.ts +97 -0
  107. package/dist/injectableComponentTypes.js +12 -0
  108. package/dist/keystore/index.d.ts +3 -0
  109. package/dist/keystore/index.js +10 -0
  110. package/dist/keystore/publicAccountStore.d.ts +14 -0
  111. package/dist/keystore/publicAccountStore.js +29 -0
  112. package/dist/keystore/secretCache.d.ts +20 -0
  113. package/dist/keystore/secretCache.js +48 -0
  114. package/dist/panels/AcknowledgeDialog.d.ts +6 -0
  115. package/dist/panels/AcknowledgeDialog.js +14 -0
  116. package/dist/panels/ActiveAccountSelectDropDown.d.ts +22 -0
  117. package/dist/panels/ActiveAccountSelectDropDown.js +57 -0
  118. package/dist/panels/AddressPanel.js +2 -1
  119. package/dist/panels/AgentSelectDropDown.js +3 -36
  120. package/dist/panels/AnonymousAvatar.js +2 -1
  121. package/dist/panels/DetailRow.d.ts +11 -0
  122. package/dist/panels/DetailRow.js +13 -0
  123. package/dist/panels/MenuTabHeaderBar.js +6 -64
  124. package/dist/panels/MeritInfoPanel.js +2 -2
  125. package/dist/panels/MeritInfoPanelReal.js +5 -3
  126. package/dist/panels/MessageDetailsSection.js +2 -1
  127. package/dist/panels/MessageLabelValueRow.js +2 -1
  128. package/dist/panels/MessagePanel.js +2 -18
  129. package/dist/panels/MessagePanelReal.d.ts +51 -1
  130. package/dist/panels/MessagePanelReal.js +123 -20
  131. package/dist/panels/NetworkDetailPanel.js +3 -46
  132. package/dist/panels/NetworkSelectDropDown.d.ts +5 -1
  133. package/dist/panels/NetworkSelectDropDown.js +8 -5
  134. package/dist/panels/PanelTitle.js +3 -67
  135. package/dist/panels/ReadOnlyMetaDataTable.js +12 -11
  136. package/dist/panels/SendTabPanel.d.ts +10 -1
  137. package/dist/panels/SendTabPanel.js +16 -4
  138. package/dist/panels/TokenAddressComponent.js +4 -6
  139. package/dist/panels/TokenDetailPanel.js +11 -48
  140. package/dist/panels/TokenSelectDropDown.d.ts +7 -1
  141. package/dist/panels/TokenSelectDropDown.js +7 -8
  142. package/dist/panels/TradingStationPanel.js +4 -15
  143. package/dist/panels/WalletAccountHeader.d.ts +5 -5
  144. package/dist/panels/WalletAccountHeader.js +11 -40
  145. package/dist/panels/WalletAccountSelectDropDown.d.ts +15 -0
  146. package/dist/panels/WalletAccountSelectDropDown.js +42 -0
  147. package/dist/panels/WalletRadioPanels.d.ts +9 -12
  148. package/dist/panels/WalletRadioPanels.js +97 -41
  149. package/dist/panels/clipboardClear.d.ts +7 -0
  150. package/dist/panels/clipboardClear.js +31 -0
  151. package/dist/panels/index.d.ts +3 -11
  152. package/dist/panels/index.js +1 -6
  153. package/dist/panels/meritWalletDataFetch.d.ts +20 -10
  154. package/dist/panels/meritWalletDataFetch.js +53 -13
  155. package/dist/panels/useChainInfo.d.ts +5 -0
  156. package/dist/panels/useChainInfo.js +28 -0
  157. package/dist/peer/peerConnection.d.ts +43 -0
  158. package/dist/peer/peerConnection.js +126 -0
  159. package/dist/platform/domPlatform.js +111 -1
  160. package/dist/platform/platform.d.ts +19 -0
  161. package/dist/provider/useMeritLink.d.ts +8 -0
  162. package/dist/provider/useMeritLink.js +8 -0
  163. package/dist/provider/useMetaMask.d.ts +11 -0
  164. package/dist/provider/useMetaMask.js +67 -0
  165. package/dist/radioPanelGroups.d.ts +8 -10
  166. package/dist/radioPanelGroups.js +47 -13
  167. package/dist/receipt/transactionReceipts.d.ts +27 -0
  168. package/dist/receipt/transactionReceipts.js +25 -0
  169. package/dist/refresh/refreshReact.d.ts +30 -0
  170. package/dist/refresh/refreshReact.js +101 -0
  171. package/dist/refresh/refreshTree.d.ts +42 -0
  172. package/dist/refresh/refreshTree.js +81 -0
  173. package/dist/rewards/ConnectedRewardsPanel.d.ts +6 -1
  174. package/dist/rewards/ConnectedRewardsPanel.js +58 -10
  175. package/dist/rewards/accountRoleFlags.d.ts +1 -0
  176. package/dist/rewards/accountRoleFlags.js +25 -0
  177. package/dist/rewards/chainClock.d.ts +17 -0
  178. package/dist/rewards/chainClock.js +63 -0
  179. package/dist/rewards/estimateRewards.d.ts +11 -3
  180. package/dist/rewards/estimateRewards.js +76 -2
  181. package/dist/security/mfaFactorsStore.d.ts +16 -0
  182. package/dist/security/mfaFactorsStore.js +55 -0
  183. package/dist/security/passkey.d.ts +20 -0
  184. package/dist/security/passkey.js +60 -0
  185. package/dist/security/signInOptions.d.ts +20 -0
  186. package/dist/security/signInOptions.js +12 -0
  187. package/dist/security/totp.d.ts +13 -0
  188. package/dist/security/totp.js +77 -0
  189. package/dist/send/SendComponent.js +2 -2
  190. package/dist/send/sendTransfer.d.ts +2 -2
  191. package/dist/send/sendTransfer.js +3 -17
  192. package/dist/session/activeAccountCore.d.ts +81 -0
  193. package/dist/session/activeAccountCore.js +57 -0
  194. package/dist/session/indexedDbKey.d.ts +12 -0
  195. package/dist/session/indexedDbKey.js +39 -0
  196. package/dist/session/persistedPassword.d.ts +23 -0
  197. package/dist/session/persistedPassword.js +65 -0
  198. package/dist/session/validateLogin.d.ts +11 -0
  199. package/dist/session/validateLogin.js +11 -0
  200. package/dist/session/walletSession.d.ts +9 -0
  201. package/dist/session/walletSession.js +10 -4
  202. package/dist/session/walletSessionReact.d.ts +1 -0
  203. package/dist/session/walletSessionReact.js +2 -1
  204. package/dist/sponsor/ConnectedSponsorPanel.d.ts +7 -0
  205. package/dist/sponsor/ConnectedSponsorPanel.js +32 -0
  206. package/dist/sponsor/ConnectedSponsorStakingList.d.ts +15 -0
  207. package/dist/sponsor/ConnectedSponsorStakingList.js +205 -35
  208. package/dist/sponsor/sponsorReads.d.ts +13 -0
  209. package/dist/sponsor/sponsorReads.js +59 -0
  210. package/dist/sponsor/stakingListMode.d.ts +9 -0
  211. package/dist/sponsor/stakingListMode.js +20 -0
  212. package/dist/swap/parseDecimalAmount.d.ts +5 -0
  213. package/dist/swap/parseDecimalAmount.js +26 -0
  214. package/dist/sync/pushContextPatch.d.ts +24 -0
  215. package/dist/sync/pushContextPatch.js +19 -0
  216. package/dist/sync/syncToken.d.ts +52 -0
  217. package/dist/sync/syncToken.js +128 -0
  218. package/dist/trade/tradeHost.d.ts +37 -0
  219. package/dist/trade/tradeHost.js +29 -0
  220. package/dist/vault/providerController.d.ts +21 -0
  221. package/dist/vault/providerController.js +93 -9
  222. package/dist/vault/vaultCapability.d.ts +4 -0
  223. package/dist/vault/vaultCapability.js +5 -1
  224. package/dist/vault/walletAccounts.d.ts +9 -1
  225. package/dist/vault/walletAccounts.js +27 -3
  226. package/dist/vault/workerVault.js +7 -1
  227. package/dist/wallet/AccountSigningSection.js +4 -5
  228. package/dist/wallet/WalletBackupSection.js +2 -4
  229. package/dist/wallet/accountHolders.d.ts +9 -0
  230. package/dist/wallet/accountHolders.js +22 -0
  231. package/dist/wallet/accountSigning.js +6 -1
  232. package/dist/wallet/approverAccounts.d.ts +42 -2
  233. package/dist/wallet/approverAccounts.js +110 -19
  234. package/dist/wallet/approverAccountsReact.d.ts +3 -0
  235. package/dist/wallet/approverAccountsReact.js +118 -18
  236. package/dist/wallet/sameAddress.d.ts +1 -0
  237. package/dist/wallet/sameAddress.js +4 -0
  238. package/dist/wallet/wallet.d.ts +10 -2
  239. package/dist/wallet/wallet.js +61 -28
  240. package/dist/wallet/walletSlugs.d.ts +2 -0
  241. package/dist/wallet/walletSlugs.js +7 -0
  242. package/dist/walletConfig.d.ts +13 -0
  243. package/dist/walletConfig.js +53 -4
  244. package/dist/walletConfigPanelProps.d.ts +7 -1
  245. package/dist/walletConfigPanelProps.js +17 -5
  246. package/dist/walletData.d.ts +2 -2
  247. package/package.json +4 -7
  248. package/dist/ConnectedPasswordPanel.d.ts +0 -13
  249. package/dist/ConnectedPasswordPanel.js +0 -30
  250. package/dist/WalletOverlayHost.d.ts +0 -35
  251. package/dist/WalletOverlayHost.js +0 -46
  252. package/dist/account/accountProfileClient.d.ts +0 -18
  253. package/dist/account/accountProfileClient.js +0 -50
  254. package/dist/auth/ApprovalWaitingNotice.d.ts +0 -4
  255. package/dist/auth/ApprovalWaitingNotice.js +0 -20
  256. package/dist/panels/DetailPanelEmptyState.d.ts +0 -10
  257. package/dist/panels/DetailPanelEmptyState.js +0 -19
  258. package/dist/panels/GenericListPanel.d.ts +0 -15
  259. package/dist/panels/GenericListPanel.js +0 -33
  260. package/dist/panels/RewardsPendingByAccountTypePanel.d.ts +0 -9
  261. package/dist/panels/RewardsPendingByAccountTypePanel.js +0 -39
  262. package/dist/panels/SendAddressHeaderBar.d.ts +0 -14
  263. package/dist/panels/SendAddressHeaderBar.js +0 -80
  264. package/dist/panels/SendTitle.d.ts +0 -1
  265. package/dist/panels/SendTitle.js +0 -13
  266. package/dist/panels/SendToAddressComponent.d.ts +0 -5
  267. package/dist/panels/SendToAddressComponent.js +0 -13
@@ -33,61 +33,64 @@
33
33
  // Whether/when to render this component at all stays the host's call.
34
34
  'use client';
35
35
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
36
- import { walletColors } from '@sponsorcoin/spcoin-common/styles';
36
+ import { lockLogoStyle, fitImageStyle, walletFrameStyle, walletFrameCss, walletFrameClassName } from '@sponsorcoin/spcoin-common/styles';
37
37
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
38
- import { getAddress } from 'viem';
38
+ import { usePeerStates } from './peer/peerConnection';
39
+ import { WALLET_SLUG_ID } from './wallet/walletSlugs';
40
+ import { getAddress, formatUnits } from 'viem';
39
41
  import { SP_COIN_DISPLAY } from '@sponsorcoin/spcoin-common/panels';
40
42
  import { TRADE_DIRECTION, FEED_TYPE } from '@sponsorcoin/spcoin-common/context';
41
- import { usePanelTree, usePanelVisible, panelStore, useEnforceRadioPanelGroups, useEnforcePanelAncestorVisibility, useEnforceRadioPanelContainers, useAgentAccount, useSellTokenContract, useBuyTokenContract, useSlippage, useTradeDirection, useSellAmount, useBuyAmount, activeListPanelStore, } from '@sponsorcoin/spcoin-exchange-engine';
42
- import { PanelGate, WalletPanel, WalletSelectDropDownHosted, setWalletAssetBaseUrl, useWalletListTitle, WalletConnectProvider, ConfigSlippagePanel, PACKAGE_BUILD, SHOW_BUILD_MARKERS, LoginHeader, WalletNetworkHeader, ManageSponsorshipsPanel, AgentHeaderPanel, PasswordPanel, AssetListTable, AccountListCard, SponsorStakingListPanel, AssetListSelectPanel, PanelTreePanel, WalletBalanceContext, useFetchedBalance, fetchedBalanceText, NetworkListTable, WalletConfigPanel } from '@sponsorcoin/spcoin-panels';
43
+ import { registerAccountDetailOpener, registerPreOpenAction, setAccountStoreBaseUrl, engineTrace, plainSendStepStore, stepThroughApprovalStore, stepPanelContentStore, stepPanelTitleStore, useStepPanel } from '@sponsorcoin/spcoin-exchange-engine';
44
+ import { StakeConfirmPopup, useWalletListed, amountGate } from '@sponsorcoin/spcoin-panels';
45
+ import { usePanelTree, usePanelVisible, useOpenActiveListPanel, useActiveListPanelChild, useEnforceRadioPanelGroups, useEnforcePanelAncestorVisibility, useAgentAccount, useActiveAccount, useSelectedAccount, buildSpCoinAccount, hydrateAccountFromAddress, useExchangeContext, useSellTokenContract, useBuyTokenContract, useSlippage, useTradeDirection, useSellAmount, useBuyAmount, activeListPanelStore, panelTreeTrace, useDisplayStack, usePreviewTokenContract, clampDisplay, isIntermediateDecimal, maxInputSz, TYPING_GRACE_MS, } from '@sponsorcoin/spcoin-exchange-engine';
46
+ import { AssetSelectListPanel as AssetSelectListPanelHost, PanelGate, UniSelectPanelWithSwap, WalletPanel, WalletSelectDropDownHosted, setWalletAssetBaseUrl, useWalletListTitle, WalletConnectProvider, ConfigSlippagePanel, LoginHeader, WalletNetworkHeader, ManageSponsorshipsPanel, AgentHeaderPanel, PasswordPanel, AccountListCard, SponsorStakingListPanel, AssetListSelectPanel, PanelTreePanel, WalletBalanceContext, useFetchedBalanceResult, fetchedBalanceText, NetworkListTable, WalletConfigPanel, TokenLogo, AccountAvatar } from '@sponsorcoin/spcoin-panels';
43
47
  import { domPlatform } from './platform/domPlatform';
44
- import { MeritPlatformContext } from './platform/platform';
48
+ import { MeritPlatformContext, usePlatform } from './platform/platform';
49
+ import { RefreshTreeProvider, ImageCacheContext } from './refresh/refreshReact';
50
+ import { createRefreshTree } from './refresh/refreshTree';
45
51
  import { ALL_WALLET_DERIVATIONS, mergeWalletProps, useWalletDerivedProps } from './wallet/walletProps';
46
52
  import { useApproverAccounts } from './wallet/approverAccountsReact';
47
53
  import { useApprovalMessagePanel } from './auth/useApprovalMessagePanel';
54
+ import { accountNoticeStore } from './auth/accountNoticeStore';
55
+ import { useAccountNoticeMessagePanel } from './auth/useAccountNoticeMessagePanel';
56
+ import { useChainInfo } from './panels/useChainInfo';
48
57
  import { walletName } from './auth/authenticator';
49
- import { createMeritWalletIconCaches, buildNetworkRows as selfFetchNetworkRows, fetchAccountGroups as selfFetchAccountGroups, buildRecipientRows as selfFetchRecipientRows, buildAllAccountRows as selfFetchAllAccountRows, buildTokenRows as selfFetchTokenRows, NetworkSelectDropDown, WalletAccountHeader, PanelTitle, MenuTabHeaderBar, WalletRadioPanels, TradingStationPanel, SendTabPanel, AddressPanel, TokenDetailPanel, NetworkDetailPanel } from './panels';
58
+ import { STATUS } from '@sponsorcoin/spcoin-common/context';
59
+ import { createMeritWalletIconCaches, buildNetworkRows as selfFetchNetworkRows, fetchAccountGroups as selfFetchAccountGroups, buildTokenRows as selfFetchTokenRows, NetworkSelectDropDown, WalletAccountHeader, PanelTitle, MenuTabHeaderBar, WalletRadioPanels, TradingStationPanel, SendTabPanel, MeritInfoPanelReal } from './panels';
60
+ import TokenPanel from './TokenPanel';
61
+ import ConnectedNetworkPanel from './ConnectedNetworkPanel';
50
62
  // Panel visibility in this component goes through exactly one engine:
51
63
  // @sponsorcoin/spcoin-exchange-engine's panelStore, read via PanelGate /
52
64
  // usePanelVisible. The package's own separate store (panelState.ts) was
53
65
  // deleted 2026-10-02 — see this package's index.ts header for why a
54
66
  // second store let a tree toggle silently do nothing.
55
67
  import ConnectedMessagePanel from './ConnectedMessagePanel';
68
+ import { useMeritApprovalMessagePanel } from './approval/useMeritApprovalMessagePanel';
69
+ import { pendingApprovalMessageStore } from './approval/pendingApprovalMessageStore';
56
70
  import ConnectedRewardsPanel from './rewards/ConnectedRewardsPanel';
57
71
  import AddAccountFlow from './account/AddAccountFlow';
58
72
  import TestAccountsSection from './TestAccountsSection';
73
+ import ConnectedSponsorPanel from './sponsor/ConnectedSponsorPanel';
74
+ import { ConnectedTradeButton } from './trade/tradeHost';
59
75
  import WalletSessionDialogs, { sessionConfigHandlers } from './config/WalletSessionDialogs';
60
76
  import AccountDetailScreen from './account/AccountDetailScreen';
77
+ import { resolveAccountSource } from './account/accountSource';
61
78
  import RecoveryPhraseScreen from './RecoveryPhraseScreen';
79
+ import SecuritySetupScreen from './SecuritySetupScreen';
80
+ import SecurityWarningScreen from './SecurityWarningScreen';
81
+ import UnlockSecondStep from './UnlockSecondStep';
82
+ import { setStakingListMode, useStakingListMode } from './sponsor/stakingListMode';
83
+ import { anySignInStep, requiredSignInSteps } from './security/signInOptions';
84
+ import WalletOnboardingPanel from './WalletOnboardingPanel';
62
85
  import ConnectedSponsorStakingList from './sponsor/ConnectedSponsorStakingList';
63
- import { buildSendReceipt } from './receipt/transactionReceipts';
86
+ import { buildSendReceipt, toMessageAccount } from './receipt/transactionReceipts';
64
87
  import { isNativePlaceholder } from './send/sendTransfer';
88
+ import { parseDecimalAmount } from './swap/parseDecimalAmount';
65
89
  import { useTransactionReceipt } from './receipt/useTransactionReceipt';
66
90
  import { ActiveAccountProfileContext } from './swap/activeAccountProfile';
67
91
  import ConnectedAgentSelectDropDown from './AgentSelectDropDown';
68
- import { classifyAddressText, useNativeToken } from '@sponsorcoin/spcoin-exchange-engine';
69
- // 2026-09-30, Stage B (real trade wiring) — a minimal, dependency-free
70
- // decimal-string -> bigint parser for the SWAP tab's amount inputs. No
71
- // `ethers`/`viem` import here on purpose: this is a portable package (the
72
- // extension's Vite bundle included), and a full BigNumber-parsing library
73
- // is unnecessary weight for one small, well-understood conversion. Returns
74
- // undefined (never throws) for anything not yet a complete number — a
75
- // mid-type state like "12." or "" — so the caller can just skip pushing to
76
- // real ExchangeContext that keystroke and keep the local text as typed.
77
- function parseDecimalAmount(text, decimals) {
78
- const trimmed = text.trim();
79
- if (!trimmed || trimmed === '.' || !/^\d*\.?\d*$/.test(trimmed))
80
- return undefined;
81
- const [wholeRaw, fracRaw = ''] = trimmed.split('.');
82
- const whole = wholeRaw || '0';
83
- const frac = fracRaw.slice(0, decimals).padEnd(decimals, '0');
84
- try {
85
- return BigInt(whole) * BigInt(10) ** BigInt(decimals) + BigInt(frac || '0');
86
- }
87
- catch {
88
- return undefined;
89
- }
90
- }
92
+ import { useNativeToken } from '@sponsorcoin/spcoin-exchange-engine';
93
+ // parseDecimalAmount (decimal text -> bigint, an empty box is 0) lives in ./swap/parseDecimalAmount.ts.
91
94
  // 2026-09-21, Path A ("single source of truth") — the 5-tab strip's own
92
95
  // real panel-tree ids, all confirmed members of RADIO_PANEL_GROUPS'
93
96
  // MAIN_RADIO_OVERLAY_PANELS group (verified by reading panelGroups.ts
@@ -102,9 +105,9 @@ const TAB_PANEL_IDS = {
102
105
  CONFIG: SP_COIN_DISPLAY.WALLET_CONFIG_PANEL,
103
106
  };
104
107
  const TAB_KEY_BY_PANEL_ID = Object.fromEntries(Object.entries(TAB_PANEL_IDS).map(([tab, id]) => [id, tab]));
105
- import SwapTradeButton from './swap/SwapTradeButton';
106
- import ConnectedUniSelectPanel from './swap/ConnectedUniSelectPanel';
107
108
  import { RADIO_PANEL_GROUPS_WITH_FALLBACKS, EMPTY_RADIO_PANEL_GROUPS } from './radioPanelGroups';
109
+ import { PersistedTimeoutPicker } from './walletConfigPanelProps';
110
+ import { DebugBugButton, DebugConfigSection, useDebugRuntime } from './debug/DebugBug';
108
111
  // 2026-09-21, Path A — which real id each ActiveListMode value opens.
109
112
  // Several modes share one list TYPE (sellToken/buyToken/sendToken/
110
113
  // sponsorPayToken all open the same token list) — distinguishing WHICH
@@ -147,6 +150,11 @@ import { RADIO_PANEL_GROUPS_WITH_FALLBACKS, EMPTY_RADIO_PANEL_GROUPS } from './r
147
150
  // recipient picker. Preserved here as two distinct private ids
148
151
  // (MERIT_WALLET_SEND_RECIPIENT_LIST_OVERLAY vs.
149
152
  // MERIT_WALLET_SPONSOR_RECIPIENT_LIST_OVERLAY) for the same reason.
153
+ // Both since retired (2026-10-23, docs/meritFixes.txt §13-14) — neither
154
+ // name exists in the enum anymore; sendRecipient now opens the real
155
+ // REMOTE_ACCOUNT_SEND_LIST, sponsorRecipient's own mode was removed
156
+ // earlier still (2026-10-11) when the Sponsor tab moved onto
157
+ // REMOTE_ACCOUNT_RECIPIENT_LIST directly.
150
158
  // 2026-10-03 — token/account feeds return lowercase addresses, while the web
151
159
  // app shows EIP-55 checksummed ones (0xEe…EEeE vs 0xee…eeee for the very same
152
160
  // token). Normalize once, where a pick is committed, so every slot displays the
@@ -161,47 +169,34 @@ function toChecksumAddress(address) {
161
169
  return address;
162
170
  }
163
171
  }
172
+ // 2026-10-22/23, owner investigation (docs/meritFixes.txt) — attempted
173
+ // switching all six modes to the real shared ids at once; found Merit
174
+ // Wallet already mounts AssetSelectListPanel internally (sharedListHost)
175
+ // for the Sponsor tab's own shared-id pickers, and its render-priority
176
+ // chain picks sharedListHost over this component's own listOverlay
177
+ // whenever ASSET_SELECT_LIST_PANEL is visible — so switching these modes
178
+ // to real, properly-nested ids without also unifying the two renderers
179
+ // would blank Merit's own network/account/token screens, not just risk a
180
+ // cross-page collision. Reverted, then redone one mode at a time instead,
181
+ // each verified live before the next: sellToken/buyToken/sendToken first
182
+ // (REMOTE_TOKEN_LIST, see paramsForMode's own doc comment below for the
183
+ // mechanism and the stale-closure bug found along the way), confirmed
184
+ // live; then this round, sendRecipient/account/network, completing the
185
+ // set — all six now open through openActiveListPanel (see setActiveList
186
+ // below), not a bare openPanel(). account/network additionally needed
187
+ // AssetSelectListPanel to gain a host-rendered branch (renderAccountManagement/
188
+ // renderNetworkList below) since, unlike the other four, nothing in
189
+ // AssetSelectListPanel renders LOCAL_ACCOUNT_WALLET_LIST/NETWORK_LIST through
190
+ // PanelListSelectWrapper — those need the host's own wallet/network data,
191
+ // not a generic feed.
164
192
  const LIST_PANEL_ID_FOR_MODE = {
165
- sellToken: SP_COIN_DISPLAY.MERIT_WALLET_TOKEN_LIST_OVERLAY,
166
- buyToken: SP_COIN_DISPLAY.MERIT_WALLET_TOKEN_LIST_OVERLAY,
167
- sendToken: SP_COIN_DISPLAY.MERIT_WALLET_TOKEN_LIST_OVERLAY,
168
- sendRecipient: SP_COIN_DISPLAY.MERIT_WALLET_SEND_RECIPIENT_LIST_OVERLAY,
169
- account: SP_COIN_DISPLAY.MERIT_WALLET_ACCOUNT_LIST_OVERLAY,
170
- network: SP_COIN_DISPLAY.MERIT_WALLET_NETWORK_LIST_OVERLAY,
193
+ sellToken: SP_COIN_DISPLAY.REMOTE_TOKEN_LIST,
194
+ buyToken: SP_COIN_DISPLAY.REMOTE_TOKEN_LIST,
195
+ sendToken: SP_COIN_DISPLAY.REMOTE_TOKEN_LIST,
196
+ sendRecipient: SP_COIN_DISPLAY.REMOTE_ACCOUNT_SEND_LIST,
197
+ account: SP_COIN_DISPLAY.LOCAL_ACCOUNT_WALLET_LIST,
198
+ network: SP_COIN_DISPLAY.NETWORK_LIST,
171
199
  };
172
- // Every real id ANY mode above can open — used to unconditionally close
173
- // whichever one was actually open, without this component needing to
174
- // separately track "which real id is currently open" alongside
175
- // activeListMode itself (closePanel on an already-closed panel is a
176
- // no-op, so closing all of these is safe, not wasteful in any way that
177
- // matters).
178
- //
179
- // 2026-09-30 — these are now Merit Wallet's own private
180
- // MERIT_WALLET_*_LIST_OVERLAY ids (flat roots, no parent/children — see
181
- // spCoinDisplay.ts's own doc comment), not the real app's shared
182
- // REMOTE_TOKEN_LIST/ACTIVE_LIST_PANEL family. Since nothing else in the
183
- // tree declares them as a child, opening one no longer auto-reveals any
184
- // ancestor (useEnforcePanelAncestorVisibility.ts has nothing to walk up
185
- // to) — the explicit ACTIVE_LIST_PANEL/ASSET_LIST_SELECT_PANEL closes
186
- // this function used to need right after this loop are gone for the same
187
- // reason: there is no shared ancestor left for Merit Wallet to leave
188
- // stuck open.
189
- const ALL_LIST_PANEL_IDS = [
190
- SP_COIN_DISPLAY.MERIT_WALLET_TOKEN_LIST_OVERLAY,
191
- SP_COIN_DISPLAY.MERIT_WALLET_SEND_RECIPIENT_LIST_OVERLAY,
192
- SP_COIN_DISPLAY.MERIT_WALLET_ACCOUNT_LIST_OVERLAY,
193
- SP_COIN_DISPLAY.MERIT_WALLET_NETWORK_LIST_OVERLAY,
194
- ];
195
- // Static sample rows — this package has no real feed to query (see
196
- // AssetListTable.tsx's own "placeholder, not logic" doc comment); a
197
- // representative, clearly-fake set is enough to prove the shared list
198
- // shape renders and scrolls correctly, same "representative state by
199
- // default" treatment ManageSponsorshipsPanel/RewardRow already use.
200
- const SAMPLE_TOKEN_ROWS = [
201
- { id: '0xeeee', symbol: 'ETH', name: 'Ethereum', address: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' },
202
- { id: '0xspcoinv0', symbol: 'SPCOIN_V0', name: 'Sponsor Coin V0', address: '0xf3405e01f11d9d7841b4dc61f13a9834c88a5e1b' },
203
- { id: '0xweth', symbol: 'WETH', name: 'Wrapped Ether', address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' },
204
- ];
205
200
  // Sample groups for AccountListCard.tsx (LOCAL_ACCOUNT_WALLET_LIST) — same
206
201
  // "Doggie | Hot Dog" example already used elsewhere in this file's own
207
202
  // sample data. No row is flagged isActive: the header must read "Select Account" until a real account is chosen, never a sample address.
@@ -237,7 +232,9 @@ const SAMPLE_NETWORK_ROWS = [
237
232
  // the header account list now renders AccountListCard directly, which takes
238
233
  // AccountListEntry as-is. AccountListRewardsPanel remains exported by the
239
234
  // package for hosts that want the reward-aware list.
240
- function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = false, onClose, titleBadgeSrc, onRefresh, refreshing, appType, wwwIconSrc, closeIconSrc, infoIconSrc, initialActiveTab, onActiveTabChange, initialMenuOpen, onMenuOpenChange, initialOpenTarget, onOpenTargetChange, networkRows, accountGroups, accountListNotice, activeWalletName, activeWalletId, nativeWalletId, walletConnection, onDisconnectWallet, showWalletSelect = true, walletAssetBaseUrl, wallet, tokenRows, recipientRows, rewardsHost, stakingHost, accountDetailHost, addAccountHost, activeAccountAddress, activeAccountName, activeAccountSymbol, activeAccountLogoURL, chainId, baseUrl, storage, refreshToken, onAccountRowSelect, onNetworkRowSelect, onAccountIconClick, onAddAccount, swapHost, accountDetail, onTokenIconClick, tokenDetail, onNetworkIconClick, sendAmount, onSendAmountChange, sendBusy, onSendSubmit, sponsorPanel, listHost, manageRadioPanels = true, overlayHost, agentSelectSlot, fetchBalance, listPanelIdOverrides, passwordMode = 'unlock', passwordIcon, walletLocked, onResetWallet, passwordErrorText, passwordNoticeText, onPasswordSubmit, passwordSubmitting, recoveryPhrase, onRecoveryPhraseSaved, sellBalanceText, buyBalanceText, zeroXTradeButtonContent, uniSelectContent, configPasswordMode, onConfigPasswordModeChange, configPersistedTimeoutContent, configPasswordDescription, configMandatorySecurity, onConfigMandatorySecurityChange, configMandatoryApproval, onConfigMandatoryApprovalChange, configPasswordResetPanelContent, configSyncMode, onConfigSyncModeChange, configSyncDescription, configLocation, onConfigLocationChange, configShowBackgroundPage, onConfigShowBackgroundPageChange, configModalMode, onConfigModalModeChange, configSecurityPanelContent, configSigningPanelContent, configBackupPanelContent, configTestAccounts, configUniSelectVisible, onConfigUniswapEngineChange, configZeroXEngineVisible, onConfig0xEngineChange, configResetPanelsContent, configExtensionChannel, onConfigExtensionChannelChange, configExtensionDownloadPath, onConfigLogoff, onConfigResetPassword, onConfigDeleteAccount, onConfigDeleteWallet, walletSession, }) {
235
+ function MeritWalletInner({ docked = false, fullWidth = false, onClose, titleBadgeSrc, onRefresh, refreshing, appType, wwwIconSrc, closeIconSrc, infoIconSrc, initialActiveTab, onActiveTabChange, initialMenuOpen, onMenuOpenChange, initialOpenTarget, onOpenTargetChange, networkRows, accountGroups, accountListNotice, activeWalletName, activeWalletId, nativeWalletId, walletConnection, onDisconnectWallet, showWalletSelect = true, walletAssetBaseUrl, accountServiceUrl, wallet, tokenRows, rewardsHost, rewardsExtras, stakingHost, accountDetailHost, addAccountHost, remoteSelection, approvalPage, activeAccountAddress, activeAccountName, activeAccountSymbol, activeAccountLogoURL, chainId, baseUrl, storage, refreshToken, onAccountRowSelect, onNetworkRowSelect, onAccountIconClick, onAddAccount, accountDetail, onNetworkIconClick, sendAmount, onSendAmountChange, sendBusy, onSendSubmit, tradeHost, manageRadioPanels = true, fetchBalance, passwordMode = 'unlock', passwordIconSrc, approvalHost, walletLocked, onResetWallet, passwordErrorText, passwordNoticeText, onPasswordSubmit, passwordSubmitting, recoveryPhrase, onRecoveryPhraseSaved, securitySetup, sellBalanceText, buyBalanceText, configPasswordMode, onConfigPasswordModeChange, configPersistedTimeoutHours, onConfigPersistedTimeoutHoursChange, configPasswordDescription, configMandatorySecurity, onConfigMandatorySecurityChange, configMandatoryApproval, onConfigMandatoryApprovalChange, configPasswordResetPanelContent, configSyncMode, onConfigSyncModeChange, configSyncDescription, configLocation, onConfigLocationChange, configShowBackgroundPage, onConfigShowBackgroundPageChange, configModalMode, onConfigModalModeChange, configSecurityPanelContent, configSigningPanelContent, configBackupPanelContent, configTestAccounts, configUniSelectVisible, onConfigUniswapEngineChange, configZeroXEngineVisible, onConfig0xEngineChange, configResetPanelsContent, configDevToolsContent, configExtensionChannel, onConfigExtensionChannelChange, configExtensionDownloadPath, onConfigLogoff, onConfigResetPassword, onConfigDeleteAccount, onConfigDeleteWallet, walletSession, }) {
236
+ // 2026-11-14: the ladybug's hotkey, saved switch and error capture (debug/DebugBug.tsx)
237
+ useDebugRuntime();
241
238
  // 2026-09-21, Path A — activeTab is now DERIVED from the real engine's
242
239
  // activeMainOverlay (the same computed value PanelBootstrap itself
243
240
  // reads), not owned local state. useEnforceRadioPanelGroups/
@@ -248,6 +245,25 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
248
245
  // than adding a second host component sidepanel.ts would need to
249
246
  // remember to mount.
250
247
  const { activeMainOverlay, openPanel, closePanel, setPanelVisible: setRealPanelVisible, } = usePanelTree();
248
+ // 2026-10-29, on request ("it should at least open in message panel [...]
249
+ // Yes, move it into Message Panel") — replaces <ApprovalResponder
250
+ // host={approvalHost} /> (see this file's own git history): same
251
+ // decision logic, but it publishes to every ConnectedMessagePanel
252
+ // instance via pendingApprovalMessageStore instead of rendering its own
253
+ // standalone backdrop over everything — see that hook's own doc comment
254
+ // for why this call's return value (void) isn't what wires it up.
255
+ useMeritApprovalMessagePanel(approvalHost);
256
+ // 2026-10-23, owner investigation (docs/meritFixes.txt) — the sell/buy/
257
+ // send token pickers now open through the SAME mechanism Group 2's
258
+ // pickers (Sell/BuySelectPanelLayoutContainer.tsx, AgentSelectDropDown.tsx,
259
+ // RecipientSelectDropDown.tsx) already use successfully, instead of the
260
+ // raw openPanel()+local activeListMode path every other mode still uses.
261
+ // openActiveListPanel explicitly opens ASSET_SELECT_LIST_PANEL/
262
+ // ACTIVE_LIST_PANEL/the mode flag together (no reliance on a reactive
263
+ // ancestor-auto-reveal hook) and stashes the params sharedListHost's own
264
+ // PanelListSelectWrapper reads — the thing a bare enum swap alone did not
265
+ // supply, confirmed live (REMOTE_TOKEN_LIST alone opened nothing at all).
266
+ const { openActiveListPanel, closeActiveListPanel } = useOpenActiveListPanel();
251
267
  // 2026-10-09: result cards for Send / Sponsor. The active account's profile is filled in once the account rows are resolved (further down), and read at
252
268
  // the moment a result arrives.
253
269
  const showReceipt = useTransactionReceipt();
@@ -256,7 +272,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
256
272
  // panels but they did not close in the GUI... each gate should be
257
273
  // directly connected to its associated panel"). Confirmed by reading the
258
274
  // real web app's TradingStationPanel: SELL_SELECT_PANEL/ZERO_X_SELECT_
259
- // PANEL/CONFIG_SLIPPAGE_PANEL/SWAP_ARROW_BUTTON are independent panel-
275
+ // PANEL/ZERO_X_CONFIG_SLIPPAGE_PANEL/SWAP_ARROW_BUTTON are independent panel-
260
276
  // tree nodes gated via usePanelTree().isVisible(ID) there — none of them
261
277
  // touch wagmi, so they're safe to gate directly here too, the same way
262
278
  // STAKING_CONTROLLER_PANEL already is elsewhere in this file. Root cause
@@ -280,7 +296,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
280
296
  // own doc comment).
281
297
  const [sellTokenContract, setSellTokenContract] = useSellTokenContract();
282
298
  const [buyTokenContract, setBuyTokenContract] = useBuyTokenContract();
283
- const { data: slippage, setBps: setSlippageBps } = useSlippage();
299
+ const { data: slippage, setBps: setSlippageBps } = useSlippage('zeroX');
284
300
  // 2026-10-05, ZERO_X_SELECT_PANEL parity (live report: the web
285
301
  // app's buy row reads "You Receive ± 2.00% [cog]", the
286
302
  // extension's read plain "You Receive" with no cog) — the web
@@ -288,7 +304,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
288
304
  // buyLabel/onCogClick from useBuySelectPanelProps
289
305
  // (BuySelectPanelLayoutContainer.tsx: the label mirrors
290
306
  // SlippageComponent.tsx's !isSell/plain=false branch; the cog
291
- // is the buy row's only entry point into CONFIG_SLIPPAGE_PANEL).
307
+ // is the buy row's only entry point into ZERO_X_CONFIG_SLIPPAGE_PANEL).
292
308
  // This component's SWAP-tab composition never supplied either,
293
309
  // so the shared pair fell back to its plain 'You Receive:'
294
310
  // default with no cog. Both are computed here now — same
@@ -297,17 +313,17 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
297
313
  // { bps: 200, percentage: 2, percentageString: '2.00%' }), so
298
314
  // percentageString needs no fallback.
299
315
  const [tradeDirection] = useTradeDirection();
300
- const slippageCogVisible = usePanelVisible(SP_COIN_DISPLAY.CONFIG_SLIPPAGE_PANEL);
316
+ const slippageCogVisible = usePanelVisible(SP_COIN_DISPLAY.ZERO_X_CONFIG_SLIPPAGE_PANEL);
301
317
  const handleSlippageCog = useCallback(() => {
302
318
  if (slippageCogVisible) {
303
- closePanel(SP_COIN_DISPLAY.CONFIG_SLIPPAGE_PANEL, 'MeritWallet:slippageCog:close');
319
+ closePanel(SP_COIN_DISPLAY.ZERO_X_CONFIG_SLIPPAGE_PANEL, 'MeritWallet:slippageCog:close');
304
320
  }
305
321
  else {
306
- openPanel(SP_COIN_DISPLAY.CONFIG_SLIPPAGE_PANEL, 'MeritWallet:slippageCog:open');
322
+ openPanel(SP_COIN_DISPLAY.ZERO_X_CONFIG_SLIPPAGE_PANEL, 'MeritWallet:slippageCog:open');
307
323
  }
308
324
  }, [slippageCogVisible, openPanel, closePanel]);
309
325
  const buyLabel = tradeDirection === TRADE_DIRECTION.SELL_EXACT_OUT
310
- ? `You Receive ± ${slippage.percentageString}`
326
+ ? `You Receive (Zero X) ± ${slippage.percentageString}`
311
327
  : 'You Receive:';
312
328
  // 2026-09-30, real bug fix (sweep after the same class of bug was
313
329
  // confirmed live in the web app's UniSelectPanel/index.tsx, in THIS
@@ -341,11 +357,39 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
341
357
  onMenuOpenChange?.(menuTabHeaderVisible);
342
358
  }, [menuTabHeaderVisible, onMenuOpenChange]);
343
359
  const panelTreeVisible = usePanelVisible(SP_COIN_DISPLAY.PANEL_TREE_PANEL);
360
+ // 2026-10-23 (owner report: MERIT_INFO_PANEL shows empty) — this flag and
361
+ // meritInfoOverlay below were lost when WalletOverlayHost.tsx (which used
362
+ // to render `slots.meritInfoPanel ?? <MeritInfoPanelReal />` for this
363
+ // exact panel) was deleted 2026-10-18 ("the web runs on the component's
364
+ // own body (overlay host and nine panels deleted)") — every OTHER
365
+ // full-body overlay it hosted (panelTreeOverlay among them) was ported
366
+ // into this component's own body at the same time; this one was missed.
367
+ const meritInfoVisible = usePanelVisible(SP_COIN_DISPLAY.MERIT_INFO_PANEL);
368
+ // 2026-11-13 (owner): the list opened by Rewards > Unstake has its own page title
369
+ const stakingListVisible = usePanelVisible(SP_COIN_DISPLAY.SPONSOR_STAKING_LIST);
370
+ const stakingListMode = useStakingListMode();
344
371
  const messageVisible = usePanelVisible(SP_COIN_DISPLAY.MESSAGE_PANEL);
372
+ // The header pills' chevrons follow the real visibility of their list (2026-10-30, on request), so a list opened by any other means flips them too.
373
+ // A step-through popup (Swap / Send / Sponsor) is drawn in MESSAGE_PANEL: its stage heading is the PANEL_TITLE, and the tab body stays mounted.
374
+ // 2026-11-11 (owner): a confirmation that names its own heading (the Remove Account one) shows it in PANEL_TITLE while its message is up
375
+ const confirmTitle = React.useSyncExternalStore(pendingApprovalMessageStore.subscribe, () => pendingApprovalMessageStore.getSnapshot()?.panelTitle, () => undefined);
376
+ // 2026-11-11 (owner): a message that names its own heading (the Sponsor config info, "Sponsorcoin Distribution Reward") shows it in PANEL_TITLE too, in place of the screen's title
377
+ const { errorMessage: shownMessage } = useExchangeContext();
378
+ const messageOwnTitle = messageVisible ? shownMessage?.title : undefined;
379
+ const stepContentUp = React.useSyncExternalStore(stepPanelContentStore.subscribe, () => stepPanelContentStore.getSnapshot() != null, () => false) && messageVisible;
380
+ const stepTitle = React.useSyncExternalStore(stepPanelTitleStore.subscribe, stepPanelTitleStore.getSnapshot, stepPanelTitleStore.getServerSnapshot);
381
+ React.useEffect(() => {
382
+ engineTrace('MeritWallet:stepContent', { stepContentUp, messageVisible, stepTitle });
383
+ }, [stepContentUp, messageVisible, stepTitle]);
345
384
  // 2026-10-13: where the wallet asset files are served from, for the pill, the list and the panel alike (a host on another origin, the extension, passes the web app's address)
385
+ if (accountServiceUrl !== undefined)
386
+ setAccountStoreBaseUrl(accountServiceUrl); // the same kind of module setting, read by the account form's sign-in and save
346
387
  if (walletAssetBaseUrl !== undefined)
347
388
  setWalletAssetBaseUrl(walletAssetBaseUrl); // set while rendering (an idempotent module setting), so the children's first render already reads it
348
- const walletPanelVisible = usePanelVisible(SP_COIN_DISPLAY.WALLET_PANEL);
389
+ // 2026-11-01 (owner rule: a panel is drawn only while the panel tree says its branch is open). The detail screens below (account, agent, sponsor, recipient, token, network, wallet) are members of ASSET_RADIO_PANELS: their own flag outlives the branch now (closing a panel closes
390
+ // just that panel), so each is drawn only while ASSET_RADIO_PANELS itself is open. Live log: the Account Details screen kept covering a list the tree had switched to.
391
+ const assetRadioOpen = usePanelVisible(SP_COIN_DISPLAY.ASSET_RADIO_PANELS);
392
+ const walletPanelVisible = usePanelVisible(SP_COIN_DISPLAY.WALLET_PANEL) && assetRadioOpen;
349
393
  const walletLogoVisible = usePanelVisible(SP_COIN_DISPLAY.WALLET_LOGO);
350
394
  const walletMetaDataVisible = usePanelVisible(SP_COIN_DISPLAY.WALLET_META_DATA);
351
395
  const sellVisible = usePanelVisible(SP_COIN_DISPLAY.SELL_SELECT_PANEL);
@@ -359,12 +403,8 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
359
403
  // called every render, just with nothing to enforce when disabled).
360
404
  useEnforceRadioPanelGroups(manageRadioPanels ? RADIO_PANEL_GROUPS_WITH_FALLBACKS : EMPTY_RADIO_PANEL_GROUPS);
361
405
  useEnforcePanelAncestorVisibility();
362
- // 2026-10-03 — containment, and it MUST come after ancestor reveal above.
363
- // React runs effects in hook order within a commit, so a bare
364
- // setPanelVisible on one of the twelve detail panels gets its container
365
- // opened first and this pass then stands down; the other order would close
366
- // the panel again in the same flush that revealed its container.
367
- useEnforceRadioPanelContainers(manageRadioPanels ? RADIO_PANEL_GROUPS_WITH_FALLBACKS : EMPTY_RADIO_PANEL_GROUPS);
406
+ // 2026-11-01 (owner rule): closing a panel closes just that panel. The container enforcement that used to run here (it force-closed a group's
407
+ // members when their container closed) is removed. See docs/meritFixes.txt, "PANEL CLOSE RULE".
368
408
  // 2026-09-25, on request ("migrate AGENT_HEADER_PANEL") — useAgentAccount
369
409
  // is genuinely portable (already in @sponsorcoin/spcoin-exchange-engine),
370
410
  // so agentAccount here is real, live, shared state — not a placeholder —
@@ -392,13 +432,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
392
432
  const [selfFetchedNetworkRows, setSelfFetchedNetworkRows] = useState(undefined);
393
433
  const [selfFetchedAccountGroups, setSelfFetchedAccountGroups] = useState(undefined);
394
434
  const [selfFetchedTokenRows, setSelfFetchedTokenRows] = useState(undefined);
395
- // false until the token self-fetch has finished (rows or failure): until then the pickers must not show the placeholder rows as if they were the real list.
396
- const [tokenFetchSettled, setTokenFetchSettled] = useState(false);
397
- const [selfFetchedRecipientRows, setSelfFetchedRecipientRows] = useState(undefined);
398
- // 2026-10-03 — Send's recipient picker lists every known account, not the
399
- // chain's recipient directory (see buildAllAccountRows' doc comment).
400
- const [selfFetchedAllAccountRows, setSelfFetchedAllAccountRows] = useState(undefined);
401
- // Only fetches whichever of the 4 the caller did NOT already supply
435
+ // Only fetches whichever of the 3 the caller did NOT already supply
402
436
  // explicitly (checked once, at effect-run time — an explicit prop always
403
437
  // wins, self-fetch never overwrites it). Re-runs if chainId/baseUrl
404
438
  // change (e.g. the host's own wallet switches networks) OR refreshToken
@@ -413,13 +447,24 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
413
447
  // per-domain isolation meritWalletDataFetch.ts's own functions already
414
448
  // guarantee.
415
449
  const hasFetchedOnceRef = useRef(false);
450
+ // 2026-11-04: an image put in (or dropped from) the cache changes this, so the rows are read again from the cache, not refetched
451
+ const [imageVersion, setImageVersion] = useState(0);
452
+ // 2026-11-13: first-run pages after the password: the phrase, then the optional second step, then the warning page (the phrase itself is already dropped by its screen)
453
+ const [setupStage, setSetupStage] = useState('phrase');
454
+ // 2026-11-13: the password typed on the unlock screen, held in memory only while the extra sign-in steps are asked for
455
+ const [pendingUnlock, setPendingUnlock] = useState(undefined);
456
+ const imageVersionSeenRef = useRef(0);
457
+ const imagePlatform = usePlatform();
458
+ useEffect(() => imagePlatform.onImageCacheUpdated?.(() => setImageVersion((n) => n + 1)), [imagePlatform]);
416
459
  const tokenRowsChainRef = useRef(undefined);
417
460
  useEffect(() => {
418
461
  if (chainId == null)
419
462
  return;
420
463
  const caches = iconCachesRef.current;
421
464
  const resolvedBaseUrl = baseUrl ?? '';
422
- const forceRefresh = hasFetchedOnceRef.current;
465
+ const imageOnly = imageVersionSeenRef.current !== imageVersion;
466
+ imageVersionSeenRef.current = imageVersion;
467
+ const forceRefresh = hasFetchedOnceRef.current && !imageOnly;
423
468
  hasFetchedOnceRef.current = true;
424
469
  let cancelled = false;
425
470
  if (networkRows === undefined) {
@@ -429,40 +474,27 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
429
474
  .catch((error) => console.error('MeritWallet: failed to self-fetch network rows:', error));
430
475
  }
431
476
  if (accountGroups === undefined) {
432
- void selfFetchAccountGroups(chainId, resolvedBaseUrl, caches, forceRefresh)
477
+ void selfFetchAccountGroups(chainId, resolvedBaseUrl, caches, forceRefresh, (partial) => { if (!cancelled)
478
+ setSelfFetchedAccountGroups(partial); })
433
479
  .then((groups) => { if (!cancelled)
434
480
  setSelfFetchedAccountGroups(groups); })
435
481
  .catch((error) => console.error('MeritWallet: failed to self-fetch account groups:', error));
436
482
  }
437
483
  if (tokenRows === undefined) {
438
- setTokenFetchSettled(false);
439
484
  // another chain's rows must not stay up while this chain's list loads
440
485
  if (tokenRowsChainRef.current !== undefined && tokenRowsChainRef.current !== chainId)
441
486
  setSelfFetchedTokenRows(undefined);
442
487
  tokenRowsChainRef.current = chainId;
443
488
  void selfFetchTokenRows(chainId, resolvedBaseUrl, caches, forceRefresh)
444
- .then((rows) => { if (!cancelled) {
445
- setSelfFetchedTokenRows(rows);
446
- setTokenFetchSettled(true);
447
- } })
448
- .catch((error) => { console.error('MeritWallet: failed to self-fetch token rows:', error); if (!cancelled)
449
- setTokenFetchSettled(true); });
450
- }
451
- if (recipientRows === undefined) {
452
- void selfFetchRecipientRows(chainId, resolvedBaseUrl, caches, forceRefresh)
453
489
  .then((rows) => { if (!cancelled)
454
- setSelfFetchedRecipientRows(rows); })
455
- .catch((error) => console.error('MeritWallet: failed to self-fetch recipient rows:', error));
490
+ setSelfFetchedTokenRows(rows); })
491
+ .catch((error) => console.error('MeritWallet: failed to self-fetch token rows:', error));
456
492
  }
457
- void selfFetchAllAccountRows(resolvedBaseUrl, caches, forceRefresh)
458
- .then((rows) => { if (!cancelled)
459
- setSelfFetchedAllAccountRows(rows); })
460
- .catch((error) => console.error('MeritWallet: failed to self-fetch all-account rows:', error));
461
493
  return () => {
462
494
  cancelled = true;
463
495
  };
464
- // Deliberately NOT depending on networkRows/accountGroups/tokenRows/
465
- // recipientRows themselves — those are "did the caller supply this at
496
+ // Deliberately NOT depending on networkRows/accountGroups/tokenRows
497
+ // themselves — those are "did the caller supply this at
466
498
  // all" checks meant to run once per chainId/baseUrl/refreshToken
467
499
  // change, not on every render; including them would refetch every
468
500
  // time self-fetched state itself changes (an infinite loop for
@@ -470,25 +502,42 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
470
502
  // these two deps includes is exactly what a targeted lint-disable
471
503
  // here is for).
472
504
  // eslint-disable-next-line react-hooks/exhaustive-deps
473
- }, [chainId, baseUrl, refreshToken]);
505
+ }, [chainId, baseUrl, refreshToken, imageVersion]);
474
506
  // Layering: explicit prop > self-fetched > (existing SAMPLE_* fallback,
475
- // applied further down at each of these 4's own point of use, unchanged).
507
+ // applied further down at each of these 3's own point of use, unchanged).
476
508
  const resolvedNetworkRows = networkRows ?? selfFetchedNetworkRows;
477
509
  const fetchedAccountGroups = accountGroups ?? selfFetchedAccountGroups;
510
+ // 2026-10-26 (owner report: MetaMask's and the extension's own rows
511
+ // never showed "Connected", though their groups correctly showed
512
+ // ACTIVE, and only the native Merit group's row worked) -- root cause,
513
+ // confirmed via a captured trace: buildApproverGroups (approverAccounts.ts)
514
+ // already computes isActive correctly, checking BOTH the active account
515
+ // address AND which authenticator/group it belongs to
516
+ // (isActiveSource && same(entry.address, activeAddress)). This memo used
517
+ // to unconditionally RECOMPUTE isActive from scratch, by address alone,
518
+ // with no idea which authenticator a row's own address belongs to --
519
+ // a second, redundant source of truth for the exact same flag, the same
520
+ // "two places decide one thing" shape behind several other bugs this
521
+ // session. Now only fills isActive when the caller hasn't already
522
+ // decided it (the self-fetched/SAMPLE fallback path, whose accounts
523
+ // never carry the flag); the approver system's own already-correct
524
+ // value is no longer overwritten.
478
525
  const resolvedAccountGroups = useMemo(() => {
479
526
  if (activeAccountAddress === undefined || !fetchedAccountGroups)
480
527
  return fetchedAccountGroups;
481
528
  const want = activeAccountAddress.trim().toLowerCase();
482
529
  return fetchedAccountGroups.map((group) => ({
483
530
  ...group,
484
- accounts: group.accounts.map((account) => ({ ...account, isActive: !!want && String(account.address ?? '').toLowerCase() === want })),
531
+ accounts: group.accounts.map((account) => ({
532
+ ...account,
533
+ isActive: typeof account.isActive === 'boolean' ? account.isActive : !!want && String(account.address ?? '').toLowerCase() === want,
534
+ })),
485
535
  }));
486
536
  }, [fetchedAccountGroups, activeAccountAddress]);
487
537
  const resolvedTokenRows = tokenRows ?? selfFetchedTokenRows;
488
- const resolvedRecipientRows = recipientRows ?? selfFetchedRecipientRows;
489
538
  const [openTarget, setOpenTarget] = useState(initialOpenTarget ?? 'prod');
490
539
  // 2026-09-21, Path A — REAL BUG caught and fixed before shipping, not
491
- // hypothetical: `ASSET_LIST_SELECT_PANEL` (the list overlay's real
540
+ // hypothetical: `ASSET_SELECT_LIST_PANEL` (the list overlay's real
492
541
  // parent) is a `MAIN_RADIO_OVERLAY_PANELS` sibling of every trade-tab
493
542
  // panel (confirmed by reading `panelTreeCallbacks.ts`'s own `openPanel`
494
543
  // directly — opening any `ACTIVE_LIST_PANEL` child walks `PARENT_OF` up
@@ -533,42 +582,90 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
533
582
  if (derivedTab)
534
583
  lastKnownTabRef.current = derivedTab;
535
584
  const activeTab = derivedTab ?? lastKnownTabRef.current;
585
+ // 2026-10-24 (owner report, screenshots): the TAB STRIP's own highlight
586
+ // must NOT use activeTab above — that intentionally keeps remembering the
587
+ // last real tab for gatedTabBody (which tab's own content renders while a
588
+ // list/detail covers it), which is right for THAT, but wrong for what the
589
+ // tab strip visually shows: "if the associated Tab Panel is on top of the
590
+ // [real navigation] stack, then it is highlighted. if not, it is not" —
591
+ // the owner's own rule, confirmed across 7 screenshots (Swap stayed
592
+ // highlighted with MERIT_INFO_PANEL covering the body and
593
+ // TRADING_STATION_PANEL buried under WALLET_CONFIG_PANEL in the stack;
594
+ // Config stayed highlighted with ASSET_SELECT_LIST_PANEL — the network
595
+ // selector, opened from the header, not the Config tab itself — on top).
596
+ // displayStackIds (useDisplayStack, real React state, re-renders on every
597
+ // push/pop) is the actual navigation stack; its own last entry is "what
598
+ // is really on top" -- TAB_KEY_BY_PANEL_ID[that] is the tab to highlight,
599
+ // or null for none, with NO lastKnownTabRef-style fallback (that fallback
600
+ // is exactly the bug: it remembers a tab that is no longer on top).
601
+ const { displayStackIds } = useDisplayStack();
602
+ const topOfStackId = displayStackIds.length > 0 ? displayStackIds[displayStackIds.length - 1] : undefined;
603
+ const highlightedTab = topOfStackId != null ? TAB_KEY_BY_PANEL_ID[topOfStackId] ?? null : null;
536
604
  // The list owns its picker destination in the engine's parameter store;
537
605
  // this component keeps no duplicate slot state. The panel tree still owns visibility.
538
606
  const activeListModeLocal = React.useSyncExternalStore(activeListPanelStore.subscribe, activeListPanelStore.getWalletListSelectionSlot, () => null);
539
- // 2026-10-05 — the ADDRESS_PANEL's typed value for the open token/recipient list.
540
- // Cleared whenever the list changes or closes (setActiveList below).
541
- const [addressInput, setAddressInput] = useState('');
542
- // What the host's resolver made of the typed address (only for addresses NOT in the loaded
543
- // rows). `result: null` = lookup in flight. See the effect below buildAddressBar.
544
- const [entryResolution, setEntryResolution] = useState(null);
545
- const addressRowsRef = useRef([]);
546
- const listPanelIdFor = (mode) => listPanelIdOverrides?.[mode] ?? LIST_PANEL_ID_FOR_MODE[mode];
547
- const accountListTreeOpen = usePanelVisible(listPanelIdFor('account'));
548
- const networkListTreeOpen = usePanelVisible(listPanelIdFor('network'));
549
- // A tab on screen means no list is: a shared list's own flag stays set while another tab shows (it is covered, not closed), so it only counts while no tab is the visible overlay.
550
- // A tab covers a list only when the list is one of the shared radio panels (the host passed listPanelIdOverrides: opening it closed the tab's radio member, so a tab that is on screen again means the list was covered, not closed).
551
- // The wallet's own private list overlays are not radio members: the tab stays the main overlay while one is open, and the list the person just opened must show.
552
- const tabCoversList = !!derivedTab && !!listPanelIdOverrides;
553
- const activeListMode = tabCoversList ? null : activeListModeLocal ?? (accountListTreeOpen ? 'account' : networkListTreeOpen ? 'network' : null);
607
+ const listPanelIdFor = (mode) => LIST_PANEL_ID_FOR_MODE[mode];
608
+ // 2026-10-23 (docs/meritFixes.txt §15-16) — the single source of truth for
609
+ // "which ACTIVE_LIST_PANEL_MODES member is really active right now,"
610
+ // already ANDed against ASSET_SELECT_LIST_PANEL's own visibility (see
611
+ // useActiveListPanelChild's own doc comment for why that AND-gate, not an
612
+ // effect-based reset, is what actually closes this bug). Used both here
613
+ // (replacing the old accountListTreeOpen/networkListTreeOpen fallback,
614
+ // which read the two leaky child flags directly) and below, for
615
+ // sharedListHost — one read, two consumers, so they can never disagree.
616
+ const activeChild = useActiveListPanelChild();
617
+ // The wallet's own list overlays are not radio members: the tab stays the main overlay while one is open, and the list the person just opened must show.
618
+ // 2026-11-01 (owner rule: a panel is drawn only while the panel tree says it is open): the selection slot is not state about what is on screen, it is the picker destination. It counts only while the list's own panel,
619
+ // ASSET_SELECT_LIST_PANEL, is visible. Without this, a tab switch (which no longer clears the slot) left the account list drawn over the Swap tab.
620
+ const listPanelOpen = usePanelVisible(SP_COIN_DISPLAY.ASSET_SELECT_LIST_PANEL);
621
+ // 2026-11-02 (owner: closing LOCAL_ACCOUNT_WALLET_LIST in the tree left the account list drawn): the slot counts only while ITS OWN panel is the open child too, so closing that one panel removes the list from the screen.
622
+ const slotShowing = activeListModeLocal != null && activeChild === listPanelIdFor(activeListModeLocal);
623
+ const activeListMode = !listPanelOpen
624
+ ? null
625
+ : slotShowing
626
+ ? activeListModeLocal
627
+ : (activeChild === SP_COIN_DISPLAY.LOCAL_ACCOUNT_WALLET_LIST
628
+ ? 'account'
629
+ : activeChild === SP_COIN_DISPLAY.NETWORK_LIST
630
+ ? 'network'
631
+ : null);
632
+ // 2026-10-23 — all six modes now open through openActiveListPanel (see
633
+ // paramsForMode below), not a bare openPanel(): openPanel(childId) alone
634
+ // never actually opens ASSET_SELECT_LIST_PANEL (a MAIN_RADIO_OVERLAY_PANELS
635
+ // member — useEnforcePanelAncestorVisibility.ts deliberately stops its
636
+ // ancestor-reveal walk before those), so sharedListHost's own
637
+ // PanelListSelectWrapper / host-rendered branches never actually showed.
638
+ // paramsForMode is defined further down (after the state/handlers it
639
+ // closes over), but that's fine — this function isn't CALLED until some
640
+ // later click, well after this render (and paramsForMode's own
641
+ // definition) has completed.
554
642
  const setActiveList = (mode) => {
555
- setAddressInput('');
556
643
  if (mode) {
557
644
  activeListPanelStore.setWalletListSelectionSlot(mode);
558
- openPanel(listPanelIdFor(mode), 'MeritWallet:setActiveList');
645
+ openActiveListPanel(paramsForMode(mode), 'MeritWallet:setActiveList', listPanelIdFor(mode));
559
646
  }
560
647
  else {
561
648
  activeListPanelStore.setWalletListSelectionSlot(null);
562
- const overrideIds = Object.values(listPanelIdOverrides ?? {});
563
- for (const id of [...ALL_LIST_PANEL_IDS, ...overrideIds])
564
- closePanel(id, 'MeritWallet:setActiveList:close');
565
- if (panelStore.isVisible(SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL))
566
- closePanel(SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL, 'NAV_POP:MeritWallet:setActiveList:close');
649
+ // 2026-10-23 — was a hand-rolled partial close (just
650
+ // activeListPanelStore.clear() + closing ASSET_SELECT_LIST_PANEL
651
+ // itself), which never closed the specific ACTIVE_LIST_PANEL_MODES
652
+ // child that was actually open (e.g. NETWORK_LIST) — the exact root
653
+ // cause of the "pop the stack, content stays visible" bug (docs/
654
+ // meritFixes.txt §15-16). closeActiveListPanel already does this
655
+ // correctly (closes whichever mode is open, then ACTIVE_LIST_PANEL,
656
+ // then this panel, then clears the store) — same call Group 2's own
657
+ // pickers already use to close. The AND-gated useActiveListPanelChild
658
+ // hook makes every reader of activeChild immune to a stale child flag
659
+ // regardless, but other direct readers of these six flags (e.g.
660
+ // TokenPanel.tsx) are not — this is the real fix, not a belt-and-
661
+ // suspenders extra.
662
+ closeActiveListPanel('NAV_POP:MeritWallet:setActiveList:close');
567
663
  }
568
664
  };
569
665
  // A header chevron TOGGLES its list: down opens it (pushes the display stack), up closes it (pops it). Open = the local mode OR the tree flag of the list id (a host may have opened it through the tree).
570
666
  const toggleList = (mode) => {
571
- if (activeListMode === mode || panelStore.isVisible(listPanelIdFor(mode)))
667
+ // open means the list is SHOWING (its panel open AND this mode active), not that the mode's own flag is set: the flag outlives a tab switch now (closing a panel closes just that panel)
668
+ if (activeListMode === mode || activeChild === listPanelIdFor(mode))
572
669
  setActiveList(null);
573
670
  else
574
671
  setActiveList(mode);
@@ -591,9 +688,99 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
591
688
  };
592
689
  }, [resolvedTokenRows, resolvedNetworkRows, nativeMeta]);
593
690
  const sendTokenEntry = selections.sendToken ?? nativeSendEntry;
691
+ // 2026-10-30, on request ("animate the Send tab like you animated the Swap tab with the step tab on") - with the Steps checkbox checked the
692
+ // Send click opens the step popup (Confirm Send -> Send Complete, with the receipt behind a scroll) instead of sending at once. A plain
693
+ // transfer has no approve stage. The popup's state lives in plainSendStepStore (reading the receipt opens MESSAGE_PANEL).
694
+ const plainSend = React.useSyncExternalStore(plainSendStepStore.subscribe, plainSendStepStore.getSnapshot, plainSendStepStore.getServerSnapshot);
695
+ const sendRecipientProfile = {
696
+ address: selections.sendRecipient?.address,
697
+ name: selections.sendRecipient?.name,
698
+ symbol: selections.sendRecipient?.symbol,
699
+ logoURL: selections.sendRecipient?.iconSrc,
700
+ };
701
+ // The real send. deferReceipt: keep the success receipt for the popup's scroll instead of opening MESSAGE_PANEL; a failure always opens it.
702
+ const performSend = async (deferReceipt) => {
703
+ if (!onSendSubmit)
704
+ return false;
705
+ const params = {
706
+ recipientAddress: selections.sendRecipient?.address,
707
+ tokenAddress: sendTokenEntry.address,
708
+ decimals: sendTokenEntry.decimals,
709
+ tokenSymbol: sendTokenEntry.symbol,
710
+ amount: sendAmount ?? '',
711
+ };
712
+ const result = await Promise.resolve(onSendSubmit(params));
713
+ if (!result)
714
+ return false;
715
+ const receipt = buildSendReceipt({
716
+ result,
717
+ amount: params.amount,
718
+ tokenSymbol: params.tokenSymbol ?? '',
719
+ tokenAddress: isNativePlaceholder(params.tokenAddress) ? undefined : params.tokenAddress,
720
+ from: activeProfileRef.current,
721
+ to: sendRecipientProfile,
722
+ });
723
+ // A confirmed send clears the amount; a failed one leaves everything as it was.
724
+ if (result.ok)
725
+ onSendAmountChange?.('');
726
+ if (deferReceipt && result.ok) {
727
+ plainSendStepStore.set({ receipt, phase: 'done' });
728
+ return true;
729
+ }
730
+ showReceipt(receipt, 'MeritWallet:send');
731
+ return result.ok;
732
+ };
733
+ const startSend = () => {
734
+ if (stepThroughApprovalStore.get()) {
735
+ plainSendStepStore.set({ open: true, phase: 'confirm', viewing: false, viewingSeen: false, receipt: null, scrollRead: false });
736
+ return;
737
+ }
738
+ void performSend(false);
739
+ };
740
+ const confirmSendStep = async () => {
741
+ if (plainSendStepStore.get().phase === 'confirm') {
742
+ const ok = await performSend(true);
743
+ if (!ok)
744
+ plainSendStepStore.close(); // a failure has already opened its error receipt
745
+ return;
746
+ }
747
+ plainSendStepStore.close(); // done: the "Send Complete" button
748
+ };
749
+ const showSendStepReceipt = () => {
750
+ const saved = plainSendStepStore.get().receipt;
751
+ if (saved)
752
+ showReceipt(saved, 'MeritWallet:send');
753
+ plainSendStepStore.set({ scrollRead: true, viewing: true, viewingSeen: false });
754
+ };
755
+ // Reading the receipt hides the popup until MESSAGE_PANEL closes again, then it comes back exactly as it was.
756
+ useEffect(() => {
757
+ if (!plainSend.viewing)
758
+ return;
759
+ if (messageVisible) {
760
+ if (!plainSend.viewingSeen)
761
+ plainSendStepStore.set({ viewingSeen: true });
762
+ return;
763
+ }
764
+ if (plainSend.viewingSeen)
765
+ plainSendStepStore.set({ viewing: false, viewingSeen: false });
766
+ }, [plainSend.viewing, plainSend.viewingSeen, messageVisible]);
767
+ const sendStepElement = (_jsx(StakeConfirmPopup, { isOpen: true, inline: true, mode: "SPONSOR", isSpCoin: false, sendOnly: true, payTokenContract: { address: sendTokenEntry.address ?? '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE', chainId, symbol: sendTokenEntry.symbol, name: sendTokenEntry.name, decimals: sendTokenEntry.decimals, logoURL: sendTokenEntry.iconSrc }, activeAccount: toMessageAccount(activeProfileRef.current), recipientAccount: toMessageAccount(sendRecipientProfile), hasAgent: false, swapReceiptReady: plainSend.phase === 'done', swapReceiptKept: plainSend.phase === 'done', swapScrollRead: plainSend.scrollRead, onShowSwapReceipt: showSendStepReceipt, onCancel: () => plainSendStepStore.close(), onConfirm: () => {
768
+ void confirmSendStep();
769
+ }, chainId: chainId }));
770
+ useStepPanel({
771
+ element: sendStepElement,
772
+ showing: plainSend.open && !plainSend.viewing,
773
+ over: !plainSend.open,
774
+ onPanelClosed: () => plainSendStepStore.close(),
775
+ invoker: 'MeritWallet:sendStep',
776
+ });
594
777
  // 2026-10-11 (row 13): a list the shared panels opened through the panel tree (pay token, recipient, agent); the host's listHost renders it.
595
778
  useApprovalMessagePanel();
596
- const panelTreeListOpen = usePanelVisible(SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL);
779
+ const panelTreeListOpen = usePanelVisible(SP_COIN_DISPLAY.ASSET_SELECT_LIST_PANEL);
780
+ // sharedListHost itself (it needs renderNetworkList/renderAccountManagement,
781
+ // defined further down near where they used to live as listOverlay's own
782
+ // 'account'/'network' branches) is declared below, after those two
783
+ // functions — see its own doc comment there.
597
784
  // 2026-10-13 (owner request): the wallet pill belongs to the Add a Wallet/Account menu (ADD_WALLET_ACCOUNT: Create Account, Import, Connect), where it picks the wallet the account is added to, and to the wallet list
598
785
  // it opens (REMOTE_WALLET_LIST). It is NOT on the account list itself or on the tab pages.
599
786
  const addMenuVisible = usePanelVisible(SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT);
@@ -601,28 +788,19 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
601
788
  // (activeListMode), leaving the menu title over an empty body, and only the second press left.
602
789
  const importWalletLeafVisible = usePanelVisible(SP_COIN_DISPLAY.IMPORT_A_WALLET);
603
790
  const importAccountLeafVisible = usePanelVisible(SP_COIN_DISPLAY.IMPORT_AN_ACCOUNT);
604
- const connectAccountLeafVisible = usePanelVisible(SP_COIN_DISPLAY.CONNECT_ACCOUNT);
605
- const connectHardwareLeafVisible = usePanelVisible(SP_COIN_DISPLAY.CONNECT_HARDWARE_WALLET);
606
791
  const createAccountLeafVisible = usePanelVisible(SP_COIN_DISPLAY.CREATE_ACCOUNT);
607
792
  const walletListLeafVisible = usePanelVisible(SP_COIN_DISPLAY.WALLET_LIST_SELECT_PANEL);
608
- const connectExtensionLeafVisible = usePanelVisible(SP_COIN_DISPLAY.CONNECT_MERIT_EXTENSION);
609
- const addFlowTop = connectExtensionLeafVisible
610
- ? SP_COIN_DISPLAY.CONNECT_MERIT_EXTENSION
611
- : walletListLeafVisible
612
- ? SP_COIN_DISPLAY.WALLET_LIST_SELECT_PANEL
613
- : importWalletLeafVisible
614
- ? SP_COIN_DISPLAY.IMPORT_A_WALLET
615
- : importAccountLeafVisible
616
- ? SP_COIN_DISPLAY.IMPORT_AN_ACCOUNT
617
- : connectAccountLeafVisible
618
- ? SP_COIN_DISPLAY.CONNECT_ACCOUNT
619
- : connectHardwareLeafVisible
620
- ? SP_COIN_DISPLAY.CONNECT_HARDWARE_WALLET
621
- : createAccountLeafVisible
622
- ? SP_COIN_DISPLAY.CREATE_ACCOUNT
623
- : addMenuVisible
624
- ? SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT
625
- : undefined;
793
+ const addFlowTop = walletListLeafVisible
794
+ ? SP_COIN_DISPLAY.WALLET_LIST_SELECT_PANEL
795
+ : importWalletLeafVisible
796
+ ? SP_COIN_DISPLAY.IMPORT_A_WALLET
797
+ : importAccountLeafVisible
798
+ ? SP_COIN_DISPLAY.IMPORT_AN_ACCOUNT
799
+ : createAccountLeafVisible
800
+ ? SP_COIN_DISPLAY.CREATE_ACCOUNT
801
+ : addMenuVisible
802
+ ? SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT
803
+ : undefined;
626
804
  const walletListVisible = usePanelVisible(SP_COIN_DISPLAY.REMOTE_WALLET_LIST);
627
805
  // 2026-10-03 — token balances (see fetchBalance's doc comment). The active
628
806
  // account is read straight off the resolved groups here because the tab bodies
@@ -633,18 +811,27 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
633
811
  // 2026-10-05 — every row (this component's own AND any a host renders inside overlayHost) reads the same
634
812
  // fetcher + account through this context; see walletBalance.tsx.
635
813
  const walletBalanceValue = useMemo(() => ({ fetchBalance, accountAddress: activeBalanceAccount, refreshToken }), [fetchBalance, activeBalanceAccount, refreshToken]);
636
- const sendBalance = useFetchedBalance(fetchBalance, sendTokenEntry.address, activeBalanceAccount, refreshToken);
814
+ const { balance: sendBalance, loading: sendBalanceLoading } = useFetchedBalanceResult(fetchBalance, sendTokenEntry.address, activeBalanceAccount, refreshToken);
637
815
  // The recipient's balance of the token being sent (the Recipient row's line, as in the web Send tab).
638
- const recipientSendBalance = useFetchedBalance(fetchBalance, sendTokenEntry.address, selections.sendRecipient?.address, refreshToken);
639
- const sellBalance = useFetchedBalance(fetchBalance, sellTokenContract?.address, activeBalanceAccount, refreshToken);
640
- const buyBalance = useFetchedBalance(fetchBalance, buyTokenContract?.address, activeBalanceAccount, refreshToken);
641
- const fetchedSellBalanceText = fetchedBalanceText(fetchBalance, sellBalance, sellTokenContract?.decimals ?? 18);
642
- const fetchedBuyBalanceText = fetchedBalanceText(fetchBalance, buyBalance, buyTokenContract?.decimals ?? 18);
816
+ const { balance: recipientSendBalance, loading: recipientBalanceLoading } = useFetchedBalanceResult(fetchBalance, sendTokenEntry.address, selections.sendRecipient?.address, refreshToken);
817
+ const { balance: sellBalance, loading: sellBalanceLoading } = useFetchedBalanceResult(fetchBalance, sellTokenContract?.address, activeBalanceAccount, refreshToken);
818
+ const { balance: buyBalance, loading: buyBalanceLoading } = useFetchedBalanceResult(fetchBalance, buyTokenContract?.address, activeBalanceAccount, refreshToken);
819
+ // While a read is unresolved the text is just "Balance: " and the row shows a spinner (2026-10-30), instead of a bare "—".
820
+ const fetchedSellBalanceText = sellBalanceLoading ? 'Balance: ' : fetchedBalanceText(fetchBalance, sellBalance, sellTokenContract?.decimals ?? 18);
821
+ const fetchedBuyBalanceText = buyBalanceLoading ? 'Balance: ' : fetchedBalanceText(fetchBalance, buyBalance, buyTokenContract?.decimals ?? 18);
822
+ // 2026-10-27 (owner report) — on-screen balance text stays plain
823
+ // "Balance: X"; the richer "${symbol} Balance: X" still shows on hover,
824
+ // via these separate *Title* variables (TradeAmountRow's own doc comment
825
+ // has the full reasoning).
826
+ const fetchedSellBalanceTitle = sellTokenContract?.symbol && fetchedSellBalanceText ? `${sellTokenContract.symbol} ${fetchedSellBalanceText}` : undefined;
827
+ const fetchedBuyBalanceTitle = buyTokenContract?.symbol && fetchedBuyBalanceText ? `${buyTokenContract.symbol} ${fetchedBuyBalanceText}` : undefined;
643
828
  // The Send button's text and readiness are the shared computeSendButton (spcoin-panels), the same rules and the same button the web app's Send tab uses.
644
829
  const sendDecimals = sendTokenEntry.decimals ?? 18;
645
830
  const sendHasRecipient = Boolean(selections.sendRecipient?.address);
646
- const sendBalanceText = fetchedBalanceText(fetchBalance, sendBalance, sendDecimals);
647
- const recipientBalanceText = fetchedBalanceText(fetchBalance, recipientSendBalance, sendDecimals);
831
+ const sendBalanceText = sendBalanceLoading ? 'Balance: ' : fetchedBalanceText(fetchBalance, sendBalance, sendDecimals);
832
+ const sendBalanceTitle = sendTokenEntry.symbol && sendBalanceText ? `${sendTokenEntry.symbol} ${sendBalanceText}` : undefined;
833
+ const recipientBalanceText = recipientBalanceLoading ? 'Balance: ' : fetchedBalanceText(fetchBalance, recipientSendBalance, sendDecimals);
834
+ const recipientBalanceTitle = sendTokenEntry.symbol && recipientBalanceText ? `${sendTokenEntry.symbol} ${recipientBalanceText}` : undefined;
648
835
  // 2026-09-30, real bug fix (live report — "balanceOf... not working" /
649
836
  // "cannot type input in the Amount field" on the SWAP tab). Root cause:
650
837
  // unlike SEND/SPONSOR (real amount state + real onSubmit wired in
@@ -669,22 +856,69 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
669
856
  // any not-yet-a-full-number input — never throws, never blocks typing.
670
857
  const [sellAmountText, setSellAmountText] = useState('');
671
858
  const [buyAmountText, setBuyAmountText] = useState('');
672
- const [, setSellAmountContext] = useSellAmount();
673
- const [, setBuyAmountContext] = useBuyAmount();
859
+ const [sellAmountContext, setSellAmountContext] = useSellAmount();
860
+ const [buyAmountContextValue, setBuyAmountContext] = useBuyAmount();
674
861
  const sellDecimals = sellTokenContract?.decimals ?? 18;
675
862
  const buyDecimals = buyTokenContract?.decimals ?? 18;
863
+ const sellTypingUntilRef = useRef(0);
864
+ const buyTypingUntilRef = useRef(0);
676
865
  const setSellAmount = (text) => {
866
+ sellTypingUntilRef.current = Date.now() + TYPING_GRACE_MS;
677
867
  setSellAmountText(text);
678
868
  const parsed = parseDecimalAmount(text, sellDecimals);
679
869
  if (parsed !== undefined)
680
870
  setSellAmountContext(parsed);
681
871
  };
682
872
  const setBuyAmount = (text) => {
873
+ buyTypingUntilRef.current = Date.now() + TYPING_GRACE_MS;
683
874
  setBuyAmountText(text);
684
875
  const parsed = parseDecimalAmount(text, buyDecimals);
685
876
  if (parsed !== undefined)
686
877
  setBuyAmountContext(parsed);
687
878
  };
879
+ // 2026-10-27 (owner report, live screenshot — Trace Log showed
880
+ // sellTokenContract.amount already back at 0 after a Uniswap swap
881
+ // succeeded, but the "You Exactly Pay" box kept showing the pre-swap
882
+ // amount) — root cause: both hooks above used to discard the live
883
+ // context value (`const [, setSellAmountContext] = useSellAmount()`),
884
+ // so an external reset (UniSelectPanelLayoutContainer's setSwapState,
885
+ // on swap success) updated the real ExchangeContext but this panel's own
886
+ // local sellAmountText/buyAmountText strings — the only thing the input
887
+ // actually renders — had no path back to it; they only ever got written
888
+ // by the user's own typing (setSellAmount/setBuyAmount above). Same
889
+ // resync-with-retry pattern as spcoin-panels' SellSelectPanelLayoutContainer.tsx
890
+ // (that file's own 2026-10-27 fix for the identical bug one layer down):
891
+ // a grace window protects an in-flight keystroke from being overwritten
892
+ // by a stale/concurrent context read, and a retryTick forces one more
893
+ // pass once that window closes instead of silently skipping forever.
894
+ const [sellRetryTick, setSellRetryTick] = useState(0);
895
+ const [buyRetryTick, setBuyRetryTick] = useState(0);
896
+ useEffect(() => {
897
+ if (isIntermediateDecimal(sellAmountText))
898
+ return undefined;
899
+ const remaining = sellTypingUntilRef.current - Date.now();
900
+ if (remaining > 0) {
901
+ const timer = setTimeout(() => setSellRetryTick((t) => t + 1), remaining);
902
+ return () => clearTimeout(timer);
903
+ }
904
+ const formatted = clampDisplay(formatUnits(sellAmountContext ?? 0n, sellDecimals), maxInputSz);
905
+ if (sellAmountText !== formatted)
906
+ setSellAmountText(formatted);
907
+ return undefined;
908
+ }, [sellAmountContext, sellDecimals, sellAmountText, sellRetryTick]);
909
+ useEffect(() => {
910
+ if (isIntermediateDecimal(buyAmountText))
911
+ return undefined;
912
+ const remaining = buyTypingUntilRef.current - Date.now();
913
+ if (remaining > 0) {
914
+ const timer = setTimeout(() => setBuyRetryTick((t) => t + 1), remaining);
915
+ return () => clearTimeout(timer);
916
+ }
917
+ const formatted = clampDisplay(formatUnits(buyAmountContextValue ?? 0n, buyDecimals), maxInputSz);
918
+ if (buyAmountText !== formatted)
919
+ setBuyAmountText(formatted);
920
+ return undefined;
921
+ }, [buyAmountContextValue, buyDecimals, buyAmountText, buyRetryTick]);
688
922
  // Real balance TEXT is a caller-supplied prop (sellBalanceText/
689
923
  // buyBalanceText on MeritWalletProps), NOT fetched here directly.
690
924
  // @sponsorcoin/spcoin-exchange-engine's useGetBalance internally calls
@@ -706,16 +940,55 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
706
940
  // read-only), not a list to pick from, and can be reached from a
707
941
  // different trigger (the avatar icon, not the row/chevron). Null when
708
942
  // closed; a real address string while showing that account's details.
709
- const [accountDetailAddress, setAccountDetailAddress] = useState(null);
710
- const accountDetailVisible = usePanelVisible(SP_COIN_DISPLAY.ACCOUNT_PANEL);
943
+ // 2026-11-11 (owner): the account whose details show is the SELECTED account of ExchangeContext (persisted, cleared when the account is removed or the details are left), no longer a local state that was forgotten whenever the details panel hid.
944
+ const [selectedAccount, setSelectedAccount] = useSelectedAccount();
945
+ const accountDetailAddress = selectedAccount?.address ? String(selectedAccount.address) : null;
946
+ const latestSelectedRef = useRef(null);
947
+ const setAccountDetailAddress = (address) => {
948
+ latestSelectedRef.current = address ? address.toLowerCase() : null;
949
+ if (!address) {
950
+ setSelectedAccount(undefined);
951
+ return;
952
+ }
953
+ setSelectedAccount(buildSpCoinAccount({ address }));
954
+ // the full record (name, symbol, logo) a moment later, unless another account was selected meanwhile
955
+ void hydrateAccountFromAddress(address, { fallbackStatus: STATUS.INFO }).then((hydrated) => {
956
+ if (latestSelectedRef.current === address.toLowerCase())
957
+ setSelectedAccount(hydrated);
958
+ }).catch(() => undefined);
959
+ };
960
+ // 2026-11-08 (owner): the group of the row the details were opened from (the same address can be in two groups), and what the wallet said about it (the screen's title)
961
+ const [accountDetailGroup, setAccountDetailGroup] = useState(null);
962
+ const [accountDetailTitle, setAccountDetailTitle] = useState(null);
963
+ const accountDetailVisible = usePanelVisible(SP_COIN_DISPLAY.ACCOUNT_PANEL) && assetRadioOpen;
964
+ // The role detail panels (an agent, a sponsor, a recipient) show the same account detail screen for the account the exchange context holds in that role.
965
+ const agentPanelVisible = usePanelVisible(SP_COIN_DISPLAY.AGENT_PANEL) && assetRadioOpen;
966
+ const sponsorPanelVisible = usePanelVisible(SP_COIN_DISPLAY.SPONSOR_PANEL) && assetRadioOpen;
967
+ const recipientPanelVisible = usePanelVisible(SP_COIN_DISPLAY.RECIPIENT_PANEL) && assetRadioOpen;
968
+ const { exchangeContext: roleContext } = useExchangeContext();
969
+ const [activeAccountForDetail] = useActiveAccount();
711
970
  // Same idea, for the token-list's own info icon (Select a Token).
712
- const [tokenDetailAddress, setTokenDetailAddress] = useState(null);
713
- const tokenDetailVisible = usePanelVisible(SP_COIN_DISPLAY.TOKEN_PANEL);
971
+ // 2026-10-26 (owner decision, after a live report: right-clicking a token
972
+ // row opened TOKEN_PANEL -- the title bar updated, correctly pushed onto
973
+ // the displayStack -- but the content area stayed blank) --
974
+ // tokenDetailAddress/setTokenDetailAddress removed: nothing has set it to
975
+ // a real address since 2026-10-20 (openTokenDetail, the only caller that
976
+ // ever did, was removed in favor of the shared package's generic row
977
+ // click, TokenListItem.tsx's openPreview(), which sets previewToken
978
+ // instead). Rather than resurrect a second, parallel "what token is
979
+ // open" field -- the exact class of bug (two sources of truth drifting
980
+ // apart) chased everywhere else in the panel tree this session -- this
981
+ // now reads the one real source directly: the same previewToken
982
+ // TokenPanel.tsx (the package's own, separately-mounted copy) already
983
+ // correctly uses.
984
+ const [previewToken, setPreviewTokenContract] = usePreviewTokenContract();
985
+ const tokenDetailVisible = usePanelVisible(SP_COIN_DISPLAY.TOKEN_PANEL) && assetRadioOpen;
986
+ const previewTokenAddress = previewToken?.address ?? null;
714
987
  // Same idea, for the network-list's own icon (Select Network) — see
715
988
  // onNetworkIconClick's own doc comment for why this one needs no
716
989
  // separate "detail" data prop from the caller.
717
- const [networkDetailId, setNetworkDetailId] = useState(null);
718
- const networkDetailVisible = usePanelVisible(SP_COIN_DISPLAY.NETWORK_PANEL);
990
+ const [, setNetworkDetailId] = useState(null);
991
+ const networkDetailVisible = usePanelVisible(SP_COIN_DISPLAY.NETWORK_PANEL) && assetRadioOpen;
719
992
  // 2026-09-21, Path A — ACCOUNT_PANEL/TOKEN_PANEL/NETWORK_PANEL are ALL
720
993
  // `MAIN_RADIO_OVERLAY_PANELS` members too (verified the same way as
721
994
  // the tab/list-overlay pieces — read `panelGroups.ts` directly), so
@@ -724,14 +997,26 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
724
997
  // interaction as the list-overlay slice already found and fixed for.
725
998
  // These addresses/ids identify the selected detail record. The panel
726
999
  // tree flags below own whether their detail screens are visible.
727
- const openAccountDetail = (address) => {
1000
+ const openAccountDetail = (address, groupId) => {
728
1001
  setAccountDetailAddress(address);
1002
+ setAccountDetailGroup(groupId ?? null);
729
1003
  openPanel(SP_COIN_DISPLAY.ACCOUNT_PANEL, 'MeritWallet:openAccountDetail');
730
1004
  };
731
- const openTokenDetail = (address) => {
732
- setTokenDetailAddress(address);
733
- openPanel(SP_COIN_DISPLAY.TOKEN_PANEL, 'MeritWallet:openTokenDetail');
734
- };
1005
+ // 2026-10-23 — openTokenDetail (the token-row equivalent of
1006
+ // openAccountDetail/openNetworkDetail below) removed: its only caller was
1007
+ // Merit's own "Select a Token" list rendering, replaced by the shared
1008
+ // package's generic PanelListSelectWrapper (docs/meritFixes.txt §11),
1009
+ // which sets previewToken (usePreviewTokenContract) instead of calling
1010
+ // anything here. TOKEN_PANEL/tokenDetailVisible stay; the
1011
+ // tokenDetailAddress local state this comment used to also point to, and
1012
+ // the self-fetch hydration chain (selfTokenDetail/effectiveTokenDetail)
1013
+ // that fed the old flat TokenDetailPanel overlay, were removed 2026-10-26
1014
+ // alongside tokenDetailOverlay's own switch to TokenPanel.tsx (see that
1015
+ // variable's own doc comment) — TokenPanel hydrates itself.
1016
+ // 2026-11-11 (owner): the account dropdowns anywhere in the wallet (a right click on an avatar) open ACCOUNT_PANEL for their account through this, without making it the active one
1017
+ const openAccountDetailRef = useRef(openAccountDetail);
1018
+ openAccountDetailRef.current = openAccountDetail;
1019
+ React.useEffect(() => registerAccountDetailOpener((address) => openAccountDetailRef.current(address)), []);
735
1020
  const openNetworkDetail = (id) => {
736
1021
  setNetworkDetailId(id);
737
1022
  openPanel(SP_COIN_DISPLAY.NETWORK_PANEL, 'MeritWallet:openNetworkDetail');
@@ -743,24 +1028,82 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
743
1028
  const configResetPassword = sessionHandlers.onResetPassword;
744
1029
  const configDeleteAccount = sessionHandlers.onDeleteAccount;
745
1030
  const configDeleteWallet = sessionHandlers.onDeleteWallet;
746
- // List visibility and its destination stay on the display stack across a
747
- // tab switch. Detail visibility is tree-owned; these ids only retain which
748
- // record was selected and can be cleared when leaving the detail screen.
749
1031
  const handleTabClick = (tab) => {
750
- // Owner rule (docs 8.5.10): selecting the tab you are on changes nothing (what is open over it stays); selecting another tab goes through validateTab (openPanel routes tabs there), and what was open over the tab you leave stays on the stack, to come back with it.
751
- if (tab === activeTab)
752
- return;
1032
+ // Owner rule (2026-10-26): a tab click is ONLY the plain displayStack
1033
+ // push/pop rule, unconditionally, every time -- "if tab selected and is
1034
+ // on the displayStack, pop until it is on top; otherwise push it on
1035
+ // top." No "selecting the tab you're already on does nothing"
1036
+ // exception anymore (owner report: clicking Swap from "Select Network",
1037
+ // with Swap already the derived activeTab, did nothing -- the old
1038
+ // willNoOp check treated that as a no-op click, but the real rule is
1039
+ // unconditional: Swap IS on the stack, so it must pop up to it, same as
1040
+ // any other click). This also removes the onGlobalOverlay special case
1041
+ // (MERIT_INFO_PANEL/PANEL_TREE_PANEL) that existed only to patch one
1042
+ // hole in willNoOp -- with no no-op branch left, there is nothing left
1043
+ // for it to patch.
1044
+ //
1045
+ // This used to also remember and restore whatever list/detail was open
1046
+ // over each tab (docs 8.5.10, 2026-10-16, via listByTabRef/
1047
+ // detailByTabRef) -- removed after three straight bugs traced back to
1048
+ // behavior layered on TOP of the plain pop/push rule, never the rule
1049
+ // itself (confirmed correct by hand-trace every time): the old "tab
1050
+ // section" stack design, then closeActiveListPanel's own stale
1051
+ // restore, then that remember-and-reopen feature itself re-pushing a
1052
+ // list moments after validateTab had already popped it off correctly.
1053
+ panelTreeTrace('MeritWallet:handleTabClick', {
1054
+ tab,
1055
+ activeTab,
1056
+ activeMainOverlay: activeMainOverlay != null ? SP_COIN_DISPLAY[activeMainOverlay] : null,
1057
+ });
753
1058
  openPanel(TAB_PANEL_IDS[tab], 'MeritWallet:handleTabClick');
754
- // The wallet's own private list overlays are not stack members (they are not radio panels): leaving the tab closes them, or the list would stay on screen over the tab just chosen. Shared lists (listPanelIdOverrides) stay on the stack as the rule above says.
755
- if (!listPanelIdOverrides)
756
- setActiveList(null);
757
- // Lists are stack members and stay available when returning to this tab;
758
- // selected detail records are data only and are cleared on tab changes.
759
- setAddressInput('');
760
- setAccountDetailAddress(null);
761
- setTokenDetailAddress(null);
762
- setNetworkDetailId(null);
1059
+ // 2026-11-01 (owner rule: "closing a radio panel, or any other panel, closes just that panel, nothing else") -- a tab switch only
1060
+ // closes the panels the plain pop/push rule above closes (ASSET_SELECT_LIST_PANEL goes off the stack). The list's own ACTIVE_LIST_PANEL,
1061
+ // its open mode (NETWORK_LIST, ...) and the selection slot are left exactly as they were, so going back shows the list as it was left.
1062
+ // This used to call closeActiveListPanel(..., { keepContainer: true }) here, which wiped them.
763
1063
  };
1064
+ // The selected detail record is data tied to its panel: it goes when the panel closes, not when the tab changes.
1065
+ // 2026-11-11 (owner report: Remove opened its confirmation in MESSAGE_PANEL, backing out showed the ACTIVE account's details instead of the account that was open): the confirmation hides the details panel while it is up, and this used to forget
1066
+ // which account was open at that moment. While the message is up the record is kept; it goes only when neither the details nor a message are showing, and a beat later, so that going back (the message closes and the details open in the same breath)
1067
+ // finds it still there.
1068
+ const accountDetailVisibleRef = useRef(accountDetailVisible);
1069
+ accountDetailVisibleRef.current = accountDetailVisible;
1070
+ useEffect(() => {
1071
+ if (accountDetailVisible || messageVisible)
1072
+ return undefined;
1073
+ const timer = setTimeout(() => {
1074
+ if (accountDetailVisibleRef.current)
1075
+ return;
1076
+ setAccountDetailGroup(null);
1077
+ setAccountDetailTitle(null);
1078
+ }, 60);
1079
+ return () => clearTimeout(timer);
1080
+ }, [accountDetailVisible, messageVisible]);
1081
+ // 2026-10-26 (owner report, trace-confirmed: right-clicking a token row
1082
+ // still showed a blank body even after tokenDetailOverlay was switched to
1083
+ // read previewToken) -- a plain `!tokenDetailVisible` check (this
1084
+ // effect's own first version) fires on the VERY FIRST render after
1085
+ // openPreview() sets previewToken, before TOKEN_PANEL's own visibility
1086
+ // write has landed (that's a SEPARATE, later-scheduled write --
1087
+ // schedule(), panelTreeDebug.ts, never batches). At that instant
1088
+ // tokenDetailVisible still reads false, so this effect saw "not open,
1089
+ // but there's a preview token -- must be stale" and cleared it
1090
+ // immediately, before tokenDetailVisible ever got the chance to become
1091
+ // true. By the time it did, previewToken was already gone again -- the
1092
+ // two conditions tokenDetailOverlay needs were never true at the same
1093
+ // moment. Same fix TokenPanel.tsx's own version of this effect already
1094
+ // uses: a wasVisible ref, so this only clears on a genuine
1095
+ // was-open-now-closed transition, never on "hasn't opened yet."
1096
+ const tokenDetailWasVisibleRef = useRef(false);
1097
+ useEffect(() => {
1098
+ const wasVisible = tokenDetailWasVisibleRef.current;
1099
+ tokenDetailWasVisibleRef.current = tokenDetailVisible;
1100
+ if (!wasVisible || tokenDetailVisible)
1101
+ return;
1102
+ if (previewToken)
1103
+ setPreviewTokenContract(undefined);
1104
+ }, [tokenDetailVisible, previewToken, setPreviewTokenContract]);
1105
+ useEffect(() => { if (!networkDetailVisible)
1106
+ setNetworkDetailId(null); }, [networkDetailVisible]);
764
1107
  // 2026-09-21, Path A — initialActiveTab/onActiveTabChange used to be a
765
1108
  // plain "seed local state, notify on every setActiveTab" pair; now that
766
1109
  // activeTab is derived from the real engine, this becomes "seed the
@@ -868,87 +1211,155 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
868
1211
  // (an overlay that fully owns the panel body while open, not a layered
869
1212
  // popover). onSelect closes it the same way a real row pick would.
870
1213
  const closeListOverlay = () => setActiveList(null);
871
- // 2026-09-16 — the actual "commit this pick" step (see PickableSlot's
872
- // own doc comment above for the full report/reasoning). Reads
873
- // activeListMode BEFORE closing the overlay — closeListOverlay only
874
- // schedules the state update, it doesn't mutate this render's own
875
- // `activeListMode` binding, so capturing it first (implicitly, by
876
- // reading it in this same synchronous call) is safe and correct.
877
- const commitSelection = (pickedRow) => {
878
- const row = { ...pickedRow, address: toChecksumAddress(pickedRow.address) };
879
- const slot = activeListMode;
880
- closeListOverlay();
881
- if (!slot)
882
- return;
883
- // 2026-09-30 — sell/buy picks now write the real shared ExchangeContext
884
- // (see sellTokenContract's own doc comment near usePanelTree() above)
885
- // instead of this component's own local `selections` state, so every
886
- // other consumer of the same context (the web app's own main Swap tab,
887
- // the real balance fetch, future Uniswap/0x quote wiring) sees the pick
888
- // immediately. Every other slot (send/sponsor tokens+recipients) stays
889
- // on local `selections` — genuinely Merit-Wallet-only picks with no
890
- // real ExchangeContext counterpart today. balance defaults to 0n here,
891
- // same as liteProvider.tsx's own makeMinimalAccountFallback — real
892
- // balance display is the caller-supplied sellBalanceText/buyBalanceText
893
- // prop, not this field.
894
- if (slot === 'sellToken' || slot === 'buyToken') {
895
- const contract = row.address
896
- ? {
897
- address: row.address,
898
- symbol: row.symbol,
899
- name: row.name,
900
- decimals: row.decimals,
901
- logoURL: row.iconSrc,
902
- balance: BigInt(0),
903
- }
904
- : undefined;
905
- if (slot === 'sellToken')
906
- setSellTokenContract(contract);
907
- else
908
- setBuyTokenContract(contract);
1214
+ // the remote's account picker opens the account list by itself and closes it when the request is over
1215
+ const remoteSelectionOpen = !!remoteSelection;
1216
+ // 2026-11-06 (owner design): the selector is host state, not a panel on the stack, so it closes itself through openPanel's pre-open action: opening a tab panel ends it, like Back does (the add flow it opens from its own list does not)
1217
+ const remoteCancel = remoteSelection?.onCancel;
1218
+ useEffect(() => {
1219
+ if (!remoteCancel)
909
1220
  return;
1221
+ return registerPreOpenAction((panel) => {
1222
+ engineTrace('remoteSelection:preOpen', { panel: SP_COIN_DISPLAY[panel], closesSelector: !!TAB_KEY_BY_PANEL_ID[panel] });
1223
+ if (TAB_KEY_BY_PANEL_ID[panel])
1224
+ remoteCancel();
1225
+ });
1226
+ }, [remoteCancel]);
1227
+ useEffect(() => {
1228
+ // 2026-10-31 (owner report: the Connect request left the wallet on "Select Software Wallet" and no connect page opened): the selector is pushed ON TOP of whatever screen is open (remoteOverlay below wins the body); nothing is closed, so the screens underneath come back when it ends
1229
+ if (remoteSelectionOpen)
1230
+ setActiveList('account');
1231
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1232
+ }, [remoteSelectionOpen]);
1233
+ // 2026-10-23 — commitSelection (the "commit this pick" step PickableSlot's
1234
+ // own doc comment above describes) removed: its only callers were
1235
+ // buildAddressBar's own typed-address-entry preview rows, already removed
1236
+ // alongside it (docs/meritFixes.txt §15-16) — paramsForMode below already
1237
+ // inlines the equivalent commit logic per mode, keyed on `mode` itself
1238
+ // rather than this function's activeListMode-read (see that function's
1239
+ // own doc comment for why that distinction mattered, a real stale-closure
1240
+ // bug once).
1241
+ // 2026-10-23, owner investigation (docs/meritFixes.txt §11-12) — what
1242
+ // setActiveList hands to openActiveListPanel for each mode. sellToken/
1243
+ // buyToken/sendToken went first (see the stale-closure bug writeup this
1244
+ // function's own history carried, now folded in below): onCommit is
1245
+ // built HERE, in the same render as the click that opens the list, and
1246
+ // a pick fires much later against whichever closure was captured at
1247
+ // that moment — so it must not read anything that only becomes correct
1248
+ // after a re-render (activeListMode, read via the external
1249
+ // activeListPanelStore, is exactly that kind of value; `mode`, this
1250
+ // function's own parameter, is not — it's fixed for the whole call).
1251
+ // That's why every branch below reads `mode` directly instead of
1252
+ // routing through commitSelection's activeListMode inference.
1253
+ // account/network return null: they're host-rendered
1254
+ // (renderAccountManagement/renderNetworkList below, via
1255
+ // AssetSelectListPanel's own host-callback props) and read no feed
1256
+ // params at all — the params object exists only for modes that render
1257
+ // through the package's generic PanelListSelectWrapper.
1258
+ const paramsForMode = (mode) => {
1259
+ if (mode === 'sellToken' || mode === 'buyToken' || mode === 'sendToken') {
1260
+ return {
1261
+ feedType: FEED_TYPE.REMOTE_TOKEN_LIST,
1262
+ onCommit: (asset) => {
1263
+ const token = asset;
1264
+ const address = toChecksumAddress(token.address);
1265
+ if (mode === 'sellToken' || mode === 'buyToken') {
1266
+ const contract = address
1267
+ ? {
1268
+ address: address,
1269
+ symbol: token.symbol,
1270
+ name: token.name,
1271
+ decimals: token.decimals,
1272
+ logoURL: token.logoURL,
1273
+ balance: BigInt(0),
1274
+ }
1275
+ : undefined;
1276
+ if (mode === 'sellToken')
1277
+ setSellTokenContract(contract);
1278
+ else
1279
+ setBuyTokenContract(contract);
1280
+ }
1281
+ else {
1282
+ setSelections((prev) => ({
1283
+ ...prev,
1284
+ [mode]: {
1285
+ symbol: token.symbol,
1286
+ name: token.name,
1287
+ address,
1288
+ iconSrc: token.logoURL,
1289
+ decimals: token.decimals,
1290
+ },
1291
+ }));
1292
+ }
1293
+ closeListOverlay();
1294
+ },
1295
+ selectOnLogoClick: true,
1296
+ // the field's own token: the picker's address box opens pre-filled with it (2026-10-30, on request)
1297
+ currentTokenAddress: mode === 'sellToken' ? sellTokenContract?.address : mode === 'buyToken' ? buyTokenContract?.address : sendTokenEntry.address, // the Send field shows the native token until one is picked, so that is its token
1298
+ ownerId: mode === 'sellToken' ? 'sell' : mode === 'buyToken' ? 'buy' : 'send',
1299
+ };
910
1300
  }
911
- setSelections((prev) => ({
912
- ...prev,
913
- [slot]: {
914
- symbol: row.symbol,
915
- name: row.name,
916
- address: row.address,
917
- iconSrc: row.iconSrc,
918
- decimals: row.decimals,
919
- },
920
- }));
1301
+ if (mode === 'sendRecipient') {
1302
+ return {
1303
+ // sendRecipient's own real id (REMOTE_ACCOUNT_SEND_LIST) has a matching FEED_TYPE member exactly.
1304
+ feedType: FEED_TYPE.REMOTE_ACCOUNT_SEND_LIST,
1305
+ onCommit: (asset) => {
1306
+ const account = asset;
1307
+ const address = toChecksumAddress(account.address);
1308
+ setSelections((prev) => ({
1309
+ ...prev,
1310
+ sendRecipient: { symbol: account.symbol, name: account.name, address, iconSrc: account.logoURL },
1311
+ }));
1312
+ closeListOverlay();
1313
+ },
1314
+ // 2026-10-27 (owner report) — missing here, unlike the sellToken/
1315
+ // buyToken/sendToken block above: clicking a row's own logo/icon
1316
+ // fell through to the list's default row-click (open the account's
1317
+ // TOKEN_PANEL preview, "Select Preview Image") instead of committing
1318
+ // the recipient and returning to the Send tab. Same affordance as
1319
+ // every other picker in this function.
1320
+ selectOnLogoClick: true,
1321
+ currentTokenAddress: selections.sendRecipient?.address,
1322
+ ownerId: 'sendRecipient',
1323
+ };
1324
+ }
1325
+ // account | network
1326
+ return null;
921
1327
  };
922
- const iconFromSrc = (src) => src
923
- ? React.createElement('img', {
924
- src,
925
- alt: '',
926
- style: { width: '100%', height: '100%', objectFit: 'contain' },
927
- })
928
- : undefined;
929
1328
  // 2026-09-29 — StakingControllerPanel's own opaque-slot inputs
930
1329
  // (configCog/recipientContent) removed along with its render below
931
1330
  // (see that render site's own doc comment for why: pure duplicate
932
1331
  // content, disabled until Phase B.2 gives it something unique to show).
933
1332
  // Restore both from git history alongside the render if re-enabling.
1333
+ const sellGate = amountGate({ side: 'sell', hasToken: !!sellTokenContract?.address });
1334
+ const buyGate = amountGate({ side: 'buy', hasToken: !!buyTokenContract?.address });
934
1335
  const tabBody = activeTab === 'SWAP'
935
1336
  ? React.createElement(TradingStationPanel, {
936
1337
  onSellTokenClick: () => setActiveList('sellToken'),
937
1338
  onBuyTokenClick: () => setActiveList('buyToken'),
938
1339
  sellSymbol: sellTokenContract?.symbol,
939
1340
  sellAddress: sellTokenContract?.address,
940
- sellIcon: iconFromSrc(sellTokenContract?.logoURL),
1341
+ // 2026-10-22 (owner report): iconFromSrc returned undefined with no token picked, leaving the pill's icon slot empty — TokenLogo renders unconditionally and falls back to STATIC_ASSET_ICON_URL on its own, matching the Uniswap row's own pill (UniSelectPanelWithSwap, fed by BuySelectPanelLayoutContainer's identical fix).
1342
+ sellIcon: React.createElement(TokenLogo, { tokenContract: sellTokenContract, style: fitImageStyle }),
941
1343
  buySymbol: buyTokenContract?.symbol,
942
1344
  buyAddress: buyTokenContract?.address,
943
- buyIcon: iconFromSrc(buyTokenContract?.logoURL),
1345
+ buyIcon: React.createElement(TokenLogo, { tokenContract: buyTokenContract, style: fitImageStyle }),
944
1346
  // 2026-09-30 — real amount input + real balance reads, see their
945
1347
  // own doc comment above (near sellAmountText's declaration).
946
1348
  sellAmount: sellAmountText,
947
1349
  onSellAmountChange: setSellAmount,
1350
+ // 2026-11-13 (owner): no token in the row, no input; the hover says which token (amountGate, the one rule every amount row uses)
1351
+ sellAmountDisabled: sellGate.disabled,
1352
+ sellAmountTitle: sellGate.title,
948
1353
  sellBalanceText: sellBalanceText ?? fetchedSellBalanceText,
1354
+ sellBalanceTitle: fetchedSellBalanceTitle,
1355
+ sellBalanceLoading,
949
1356
  buyAmount: buyAmountText,
950
1357
  onBuyAmountChange: setBuyAmount,
1358
+ buyAmountDisabled: buyGate.disabled,
1359
+ buyAmountTitle: buyGate.title,
951
1360
  buyBalanceText: buyBalanceText ?? fetchedBuyBalanceText,
1361
+ buyBalanceTitle: fetchedBuyBalanceTitle,
1362
+ buyBalanceLoading,
952
1363
  // 2026-10-05 — the live, trade-direction-dependent buy
953
1364
  // label ("You Receive ± 2.00%") and the slippage cog
954
1365
  // toggle, see buyLabel/handleSlippageCog's own doc
@@ -964,7 +1375,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
964
1375
  buyVisible: zeroXVisible,
965
1376
  arrowVisible: swapArrowVisible,
966
1377
  // ConfigSlippagePanel is already fully self-gating (checks its
967
- // own usePanelVisible(CONFIG_SLIPPAGE_PANEL) internally, renders
1378
+ // own usePanelVisible(ZERO_X_CONFIG_SLIPPAGE_PANEL) internally, renders
968
1379
  // null when hidden) — no extra visibility check needed here.
969
1380
  buyPrefixContent: React.createElement(ConfigSlippagePanel, {
970
1381
  bps: slippage.bps,
@@ -984,11 +1395,9 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
984
1395
  // not a regression. (The old CONNECT_TRADE_BUTTON
985
1396
  // connection-button slot was removed 2026-10-05
986
1397
  // together with its panel-tree node.)
987
- zeroXTradeButtonContent: zeroXTradeButtonContent ?? (swapHost ? React.createElement(SwapTradeButton, { host: swapHost }) : undefined),
988
- buySuffixContent: uniSelectContent ??
989
- (swapHost
990
- ? React.createElement(ConnectedUniSelectPanel, { host: swapHost, onBuyTokenClick: () => setActiveList('buyToken'), balanceText: buyBalanceText ?? fetchedBuyBalanceText })
991
- : undefined),
1398
+ // The trade button and the Uniswap section are the shared engine-path screens, the same in every app (docs/design/spcoinPackagesDesign.txt 8.5.12).
1399
+ zeroXTradeButtonContent: React.createElement(ConnectedTradeButton, { host: tradeHost }),
1400
+ buySuffixContent: React.createElement(UniSelectPanelWithSwap),
992
1401
  })
993
1402
  : activeTab === 'SEND'
994
1403
  ? React.createElement(SendTabPanel, {
@@ -996,10 +1405,18 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
996
1405
  onRecipientClick: () => setActiveList('sendRecipient'),
997
1406
  sendTokenSymbol: sendTokenEntry.symbol,
998
1407
  sendTokenAddress: sendTokenEntry.address,
999
- sendTokenIcon: iconFromSrc(sendTokenEntry.iconSrc),
1408
+ // 2026-11-13 (owner): like the Swap pills, TokenLogo always renders; it looks the logo up by the token and falls back to the missing-token image when there is no token or no logo (no blank square after the token is cleared)
1409
+ sendTokenIcon: React.createElement(TokenLogo, { tokenContract: sendTokenEntry.address ? { address: sendTokenEntry.address, symbol: sendTokenEntry.symbol, name: sendTokenEntry.name, decimals: sendTokenEntry.decimals, logoURL: sendTokenEntry.iconSrc, chainId, balance: BigInt(0) } : undefined, style: fitImageStyle }),
1000
1410
  recipientSymbol: selections.sendRecipient?.symbol,
1001
1411
  recipientAddress: selections.sendRecipient?.address,
1002
- recipientIcon: iconFromSrc(selections.sendRecipient?.iconSrc),
1412
+ // 2026-10-22 (owner report): iconFromSrc returned undefined with no recipient picked, leaving the pill's icon slot empty — AccountAvatar renders unconditionally and falls back to ANONYMOUS_ACCOUNT_AVATAR_URL on its own, matching every other account-style pill.
1413
+ recipientIcon: React.createElement(AccountAvatar, {
1414
+ logoURL: selections.sendRecipient?.iconSrc,
1415
+ symbol: selections.sendRecipient?.symbol,
1416
+ name: selections.sendRecipient?.name,
1417
+ address: selections.sendRecipient?.address,
1418
+ style: fitImageStyle,
1419
+ }),
1003
1420
  // 2026-09-22, Phase B.2 Stage 4 follow-up — real amount input +
1004
1421
  // submit wiring (see MeritWalletProps.onSendSubmit's own doc
1005
1422
  // comment for why tokenAddress/decimals aren't fully resolved
@@ -1013,7 +1430,11 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1013
1430
  sendSymbol: sendTokenEntry.symbol ?? '',
1014
1431
  submitBusy: sendBusy,
1015
1432
  balanceText: sendBalanceText,
1433
+ balanceTitle: sendBalanceTitle,
1016
1434
  recipientBalanceText,
1435
+ recipientBalanceTitle,
1436
+ balanceLoading: sendBalanceLoading,
1437
+ recipientBalanceLoading,
1017
1438
  // 2026-09-23, Stage 39 — resolve the real sendToken address +
1018
1439
  // decimals picked via the token list picker (see PickedEntry's
1019
1440
  // own decimals doc comment) and pass them, plus the amount, to
@@ -1026,37 +1447,12 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1026
1447
  // `tsc`/`vite` verified in 2026-09-22 (no browser-automation
1027
1448
  // available), not live-driven. Native send omits tokenAddress;
1028
1449
  // an ERC20 send passes both.
1029
- onSubmit: onSendSubmit
1030
- ? () => {
1031
- const params = {
1032
- recipientAddress: selections.sendRecipient?.address,
1033
- tokenAddress: sendTokenEntry.address,
1034
- decimals: sendTokenEntry.decimals,
1035
- tokenSymbol: sendTokenEntry.symbol,
1036
- amount: sendAmount ?? '',
1037
- };
1038
- void Promise.resolve(onSendSubmit(params)).then((result) => {
1039
- if (!result)
1040
- return;
1041
- showReceipt(buildSendReceipt({
1042
- result,
1043
- amount: params.amount,
1044
- tokenSymbol: params.tokenSymbol ?? '',
1045
- tokenAddress: isNativePlaceholder(params.tokenAddress) ? undefined : params.tokenAddress,
1046
- from: activeProfileRef.current,
1047
- to: { address: selections.sendRecipient?.address, name: selections.sendRecipient?.name, symbol: selections.sendRecipient?.symbol, logoURL: selections.sendRecipient?.iconSrc },
1048
- }), 'MeritWallet:send');
1049
- // A confirmed send clears the amount; a failed one leaves everything as it was.
1050
- if (result.ok)
1051
- onSendAmountChange?.('');
1052
- });
1053
- }
1054
- : undefined,
1450
+ onSubmit: onSendSubmit ? startSend : undefined,
1055
1451
  })
1056
1452
  : activeTab === 'SPONSOR'
1057
- ? (sponsorPanel ?? null)
1453
+ ? React.createElement(ConnectedSponsorPanel, { host: tradeHost })
1058
1454
  : activeTab === 'REWARDS' && rewardsHost
1059
- ? React.createElement(ConnectedRewardsPanel, { host: rewardsHost })
1455
+ ? React.createElement(ConnectedRewardsPanel, { host: rewardsHost, extras: rewardsExtras })
1060
1456
  : activeTab === 'REWARDS'
1061
1457
  ? React.createElement(ManageSponsorshipsPanel, {
1062
1458
  // 2026-09-24, SPONSOR_STAKING_LIST EXT wiring (minimal bar) —
@@ -1066,7 +1462,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1066
1462
  // SponsorStakingListPanel itself self-gates on that same id
1067
1463
  // (see its own `panelId` default) and is mounted unconditionally
1068
1464
  // below, so no separate usePanelVisible check is needed here.
1069
- onUnstake: () => openPanel(SP_COIN_DISPLAY.SPONSOR_STAKING_LIST, 'MeritWallet:unstakeAllSponsorships'),
1465
+ onUnstake: () => { setStakingListMode({ kind: 'unstake' }); openPanel(SP_COIN_DISPLAY.SPONSOR_STAKING_LIST, 'MeritWallet:unstakeAllSponsorships'); },
1070
1466
  })
1071
1467
  : React.createElement(WalletConfigPanel, {
1072
1468
  onOpenPanelTree: () => openPanel(SP_COIN_DISPLAY.PANEL_TREE_PANEL, 'MeritWallet:openPanelTree'),
@@ -1078,7 +1474,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1078
1474
  // useMeritWalletConfig() — this component only renders it).
1079
1475
  passwordMode: configPasswordMode,
1080
1476
  onPasswordModeChange: onConfigPasswordModeChange,
1081
- persistedTimeoutContent: configPersistedTimeoutContent,
1477
+ persistedTimeoutContent: configPersistedTimeoutHours !== undefined && onConfigPersistedTimeoutHoursChange ? React.createElement(PersistedTimeoutPicker, { hours: configPersistedTimeoutHours, onChange: onConfigPersistedTimeoutHoursChange }) : undefined,
1082
1478
  passwordDescription: configPasswordDescription,
1083
1479
  mandatorySecurity: configMandatorySecurity,
1084
1480
  onMandatorySecurityChange: onConfigMandatorySecurityChange,
@@ -1094,7 +1490,8 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1094
1490
  onShowBackgroundPageChange: onConfigShowBackgroundPageChange,
1095
1491
  modalMode: configModalMode,
1096
1492
  onModalModeChange: onConfigModalModeChange,
1097
- securityPanelContent: configSecurityPanelContent,
1493
+ // 2026-11-13 (owner): the second-step rows (authenticator app / passkey) sit under the wallet's own security rows, from the host's securitySetup
1494
+ securityPanelContent: securitySetup ? (parts) => React.createElement(React.Fragment, null, React.createElement(SecuritySetupScreen, { ...securitySetup, passwordContent: parts.passwordSettings, accountButtons: parts.accountButtons }), configSecurityPanelContent) : configSecurityPanelContent,
1098
1495
  signingPanelContent: configSigningPanelContent,
1099
1496
  backupPanelContent: configBackupPanelContent,
1100
1497
  testAccountsContent: configTestAccounts ? React.createElement(TestAccountsSection, { api: configTestAccounts.api, authenticationType: configTestAccounts.authenticationType, onChanged: configTestAccounts.onChanged }) : undefined,
@@ -1103,6 +1500,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1103
1500
  zeroXEngineVisible: configZeroXEngineVisible,
1104
1501
  on0xEngineChange: onConfig0xEngineChange,
1105
1502
  resetPanelsContent: configResetPanelsContent,
1503
+ devToolsContent: React.createElement(React.Fragment, null, configDevToolsContent, React.createElement(DebugConfigSection)),
1106
1504
  extensionChannel: configExtensionChannel,
1107
1505
  onExtensionChannelChange: onConfigExtensionChannelChange,
1108
1506
  extensionDownloadPath: configExtensionDownloadPath,
@@ -1133,7 +1531,7 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1133
1531
  ? React.createElement('img', {
1134
1532
  src: account.iconSrc,
1135
1533
  alt: '',
1136
- style: { width: '100%', height: '100%', objectFit: 'contain' },
1534
+ style: fitImageStyle,
1137
1535
  })
1138
1536
  : undefined),
1139
1537
  })),
@@ -1151,6 +1549,8 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1151
1549
  // isActive flag the list overlays already use, just read once more here
1152
1550
  // rather than duplicated as separate props.
1153
1551
  const activeNetworkRow = networkRowsSource.find((row) => row.isActive);
1552
+ // the pill's "$symbol | $name" is the chain's own meta (assets/blockchains/<id>/info.json); the row's symbol and name stand in until it is read
1553
+ const activeChainInfo = useChainInfo(activeNetworkRow?.id, walletAssetBaseUrl ?? '');
1154
1554
  const flatAccountRows = effectiveAccountGroups.flatMap((group) => group.accounts);
1155
1555
  // 2026-10-13 (owner request): the wallet pill names the wallet whose group is ACTIVE in the list below it (the web's list keys its groups 'hardhat' = the dev keystore, 'metamask'), whenever the host has no
1156
1556
  // active account address to derive it from.
@@ -1158,35 +1558,37 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1158
1558
  const pillWalletId = nativeWalletId ?? activeWalletId ?? (activeGroupId === 'hardhat' ? 'serverKeystore' : activeGroupId === 'metamask' ? 'injected:io.metamask' : activeGroupId);
1159
1559
  // 2026-10-13 (owner report): the header's account pill read "Select Account" while a MetaMask account was active, because only the listed (keystore) rows were searched. The host's own active account fills in.
1160
1560
  const hostActiveAddress = String(activeAccountAddress ?? '').trim();
1561
+ // The header's avatar, name and symbol fill from the listed row for that address when the host passes none (the list already resolved its avatar), so the header never shows the anonymous placeholder for an account the list shows.
1562
+ const listedActiveRow = hostActiveAddress ? flatAccountRows.find((account) => String(account.address ?? '').toLowerCase() === hostActiveAddress.toLowerCase()) : undefined;
1161
1563
  const hostActiveEntry = hostActiveAddress
1162
1564
  ? {
1163
1565
  id: hostActiveAddress,
1164
1566
  address: hostActiveAddress,
1165
- name: activeAccountName || undefined,
1166
- symbol: activeAccountSymbol || undefined,
1167
- iconSrc: activeAccountLogoURL || undefined,
1168
- icon: activeAccountLogoURL ? React.createElement('img', { src: activeAccountLogoURL, alt: '', style: { width: '100%', height: '100%', objectFit: 'contain' } }) : undefined,
1567
+ name: activeAccountName || listedActiveRow?.name || undefined,
1568
+ symbol: activeAccountSymbol || listedActiveRow?.symbol || undefined,
1569
+ iconSrc: activeAccountLogoURL || listedActiveRow?.iconSrc || undefined,
1570
+ icon: activeAccountLogoURL ? React.createElement('img', { src: activeAccountLogoURL, alt: '', style: fitImageStyle }) : listedActiveRow?.icon,
1169
1571
  isActive: true,
1170
1572
  }
1171
1573
  : undefined;
1172
- const activeAccountEntry = flatAccountRows.find((account) => account.isActive) ?? hostActiveEntry;
1574
+ // The listed active row may know less than the host does (a per-wallet list has an address, not a profile): for the same address the host's active-account profile (name, symbol, avatar) fills what the row lacks, so the header never shows the anonymous placeholder.
1575
+ const listedActive = flatAccountRows.find((account) => account.isActive);
1576
+ const activeAccountEntry = listedActive && hostActiveEntry && String(listedActive.address ?? '').toLowerCase() === hostActiveAddress.toLowerCase()
1577
+ ? { ...listedActive, name: activeAccountName || listedActive.name, symbol: activeAccountSymbol || listedActive.symbol, iconSrc: activeAccountLogoURL || listedActive.iconSrc, icon: listedActive.icon ?? hostActiveEntry.icon }
1578
+ : listedActive ?? hostActiveEntry;
1173
1579
  activeProfileRef.current = activeAccountEntry ? { address: activeAccountEntry.address, name: activeAccountEntry.name, symbol: activeAccountEntry.symbol, logoURL: activeAccountEntry.iconSrc } : undefined;
1174
- // While the first self-fetch is still running (and no rows were passed in), the picker lists nothing and says it is loading, instead of showing the three placeholder rows and then
1175
- // swapping in the real list (and its logos) the next time it is opened. The placeholders only appear after a fetch that failed.
1176
- const tokenRowsLoading = tokenRows === undefined && resolvedTokenRows === undefined && !tokenFetchSettled;
1177
- const effectiveTokenRows = resolvedTokenRows ?? (tokenRowsLoading ? [] : SAMPLE_TOKEN_ROWS);
1178
- // 2026-09-24, ASSET_LIST_SELECT_PANEL real container (on request, after
1580
+ // 2026-09-24, ASSET_SELECT_LIST_PANEL real container (on request, after
1179
1581
  // discussion) — this whole block used to jump straight from
1180
1582
  // `activeListMode` to AssetListTable/AccountListRewardsPanel/
1181
1583
  // NetworkListTable with no intermediate container representing the real,
1182
- // distinct ASSET_LIST_SELECT_PANEL (38) panel id that sits between
1584
+ // distinct ASSET_SELECT_LIST_PANEL (38) panel id that sits between
1183
1585
  // ACTIVE_LIST_PANEL and its content children in panelRegistry.ts — a
1184
1586
  // flattened structure that didn't match the real panel-tree hierarchy.
1185
1587
  // Every branch below now routes its existing, unchanged content through
1186
1588
  // the package's own real AssetListSelectPanel shell as `listContent`
1187
1589
  // instead of rendering it directly. Deliberately NOT a cosmetic-only
1188
1590
  // change even though none of these feed types trigger the shell's
1189
- // address-bar/manage-view behavior today (same as the ASSET_LIST_SELECT_PANEL
1591
+ // address-bar/manage-view behavior today (same as the ASSET_SELECT_LIST_PANEL
1190
1592
  // investigation earlier this session found) — the point is giving the
1191
1593
  // extension a REAL, shared container matching the actual architecture, so
1192
1594
  // a future platform (iPhone/Android, per appSplitDevelopmentTime.md) reuses
@@ -1195,334 +1597,307 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1195
1597
  // `manageContent` stay omitted (no ADDRESS_PANEL/manage-view concept exists
1196
1598
  // in the extension yet) — safe, since the shell's own render guard
1197
1599
  // requires both `showAddressBar` AND a supplied `addressBar` node.
1198
- // 2026-10-05 — ADDRESS_PANEL for the extension's list overlays, the same
1199
- // shared pieces (AddressPanel = HexAddressInput + AssetPreviewRow) the web app
1200
- // draws. Entering an address NEVER leaves the list: a full address that is in
1201
- // the rows already loaded shows as a preview row, and clicking that row commits
1202
- // it (commitSelection), exactly like the web app. An address that isn't in the
1203
- // list shows a red "not in this list" row — resolving unknown addresses
1204
- // on-chain is the web app's validation FSM, not ported yet.
1205
- const buildAddressBar = (rows, kind, onInfo) => {
1206
- const typed = addressInput.trim();
1207
- let preview = null;
1208
- if (typed) {
1209
- const isFull = /^0x[0-9a-fA-F]{40}$/.test(typed);
1210
- const isPartial = /^(0|0x[0-9a-fA-F]{0,39})$/i.test(typed);
1211
- if (isFull) {
1212
- const hit = rows.find((r) => r.address?.toLowerCase() === typed.toLowerCase());
1213
- const resolved = entryResolution && entryResolution.address.toLowerCase() === typed.toLowerCase() ? entryResolution : null;
1214
- preview = hit
1215
- ? {
1216
- icon: iconFromSrc(hit.iconSrc),
1217
- symbol: hit.symbol,
1218
- name: hit.name,
1219
- address: hit.address,
1220
- onSelect: () => commitSelection(hit),
1221
- infoIconSrc,
1222
- onInfoClick: hit.address ? () => onInfo(hit.address) : undefined,
1223
- }
1224
- : resolveAssetAddress
1225
- ? resolved?.result
1226
- ? resolved.result.status === 'ok'
1227
- ? {
1228
- icon: iconFromSrc(resolved.result.asset.logoURL),
1229
- symbol: resolved.result.asset.symbol,
1230
- name: resolved.result.asset.name,
1231
- address: resolved.result.asset.address,
1232
- onSelect: () => commitSelection({
1233
- symbol: resolved.result?.status === 'ok' ? resolved.result.asset.symbol : undefined,
1234
- name: resolved.result?.status === 'ok' ? resolved.result.asset.name : undefined,
1235
- address: typed,
1236
- iconSrc: resolved.result?.status === 'ok' ? resolved.result.asset.logoURL : undefined,
1237
- decimals: resolved.result?.status === 'ok' ? resolved.result.asset.decimals : undefined,
1238
- }),
1239
- }
1240
- : {
1241
- tone: 'error',
1242
- icon: undefined,
1243
- name: 'message' in resolved.result ? resolved.result.message : 'Address not available',
1244
- address: typed,
1245
- }
1246
- : { icon: undefined, name: 'Looking up address…', address: typed }
1247
- : {
1248
- tone: 'error',
1249
- icon: undefined,
1250
- name: kind === 'token' ? 'Token not in this list' : 'Account not in this list',
1251
- address: typed,
1252
- };
1253
- }
1254
- else if (!isPartial) {
1255
- preview = { tone: 'error', icon: undefined, name: 'Invalid address', address: '' };
1256
- }
1257
- }
1258
- return React.createElement(AddressPanel, { value: addressInput, onChange: setAddressInput, preview });
1259
- };
1260
- const recipientListRows = (activeListMode === 'sendRecipient' ? selfFetchedAllAccountRows : undefined) ??
1261
- resolvedRecipientRows ??
1262
- flatAccountRows;
1263
- addressRowsRef.current =
1264
- activeListMode === 'sendRecipient' ? recipientListRows : effectiveTokenRows;
1265
- // Resolve a full address that isn't in the loaded rows (host-supplied; see resolveAssetAddress).
1266
- // The rows ref is read, not depended on, so a list refresh doesn't re-run the lookup.
1267
- useEffect(() => {
1268
- const typed = addressInput.trim();
1269
- if (!resolveAssetAddress || !activeListMode || classifyAddressText(typed) !== 'ok') {
1270
- setEntryResolution(null);
1271
- return;
1272
- }
1273
- if (addressRowsRef.current.some((r) => r.address?.toLowerCase() === typed.toLowerCase())) {
1274
- setEntryResolution(null);
1275
- return;
1276
- }
1277
- let cancelled = false;
1278
- setEntryResolution({ address: typed, result: null });
1279
- const kind = activeListMode === 'sendRecipient' ? 'account' : 'token';
1280
- const peer = activeListMode === 'sellToken'
1281
- ? buyTokenContract?.address
1282
- : activeListMode === 'buyToken'
1283
- ? sellTokenContract?.address
1284
- : undefined;
1285
- resolveAssetAddress(typed, kind, peer ? String(peer) : undefined)
1286
- .then((result) => {
1287
- if (!cancelled)
1288
- setEntryResolution({ address: typed, result });
1289
- })
1290
- .catch((error) => {
1291
- if (!cancelled)
1292
- setEntryResolution({
1293
- address: typed,
1294
- result: { status: 'error', message: error instanceof Error ? error.message : 'Lookup failed' },
1295
- });
1296
- });
1297
- return () => {
1298
- cancelled = true;
1299
- };
1300
- // eslint-disable-next-line react-hooks/exhaustive-deps
1301
- }, [addressInput, activeListMode, resolveAssetAddress]);
1302
- const listOverlay = activeListMode === 'sellToken' ||
1303
- activeListMode === 'buyToken' ||
1304
- activeListMode === 'sendToken'
1305
- ? React.createElement(AssetListSelectPanel, {
1306
- containerType: SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL,
1307
- feedType: FEED_TYPE.REMOTE_TOKEN_LIST,
1308
- addressBar: buildAddressBar(effectiveTokenRows, 'token', (address) => {
1309
- openTokenDetail(address);
1310
- onTokenIconClick?.(address);
1311
- }),
1312
- listContent: React.createElement(AssetListTable, {
1313
- rows: effectiveTokenRows.map((row) => ({
1314
- ...row,
1315
- onSelect: () => commitSelection(row),
1316
- infoIconSrc,
1317
- onInfoClick: row.address
1600
+ // 2026-10-23 — buildAddressBar (the ADDRESS_PANEL typed-address-entry
1601
+ // mechanism this comment used to describe, for sellToken/buyToken/
1602
+ // sendToken/sendRecipient's own rows) removed along with
1603
+ // resolveAssetAddress/entryResolution/addressRowsRef/addressInput: its
1604
+ // only callers were those same four modes' old Merit-local list
1605
+ // rendering, already replaced by the shared package's own
1606
+ // PanelListSelectWrapper (docs/meritFixes.txt §11-12), which has its own
1607
+ // equivalent typed-entry handling — nothing in this file needs this
1608
+ // apparatus anymore. account/network never used it (no addressBar prop
1609
+ // on their own AssetListSelectPanel calls below).
1610
+ // 2026-10-23 — sellToken/buyToken/sendToken/sendRecipient used to render
1611
+ // here too (Merit's own AssetListSelectPanel+AssetListTable, fed by
1612
+ // effectiveTokenRows/recipientListRows). Now that those four modes open
1613
+ // the real REMOTE_TOKEN_LIST/REMOTE_ACCOUNT_SEND_LIST ids via
1614
+ // openActiveListPanel (see paramsForMode above), ASSET_SELECT_LIST_PANEL
1615
+ // is already visible by the time this component would pick a branch, so
1616
+ // the body chain's own `activeChild != null ? sharedListHost : null` wins
1617
+ // before `listOverlay` is even consulted — those branches could never
1618
+ // render again, so they're removed rather than left as dead code.
1619
+ //
1620
+ // account/network keep a listOverlay fallback (reusing these same two
1621
+ // render functions, so there is only ever one copy of this JSX), but its
1622
+ // trigger narrowed with §15-16's redesign: `activeListMode`'s own
1623
+ // 'account'/'network' derivation now comes from activeChild (AND-gated
1624
+ // against ASSET_SELECT_LIST_PANEL's own visibility, see
1625
+ // useActiveListPanelChild's doc comment), not from reading
1626
+ // LOCAL_ACCOUNT_WALLET_LIST/NETWORK_LIST's raw flags directly the way it
1627
+ // used to. That means a host that set only the child flag without also
1628
+ // opening ASSET_SELECT_LIST_PANEL itself (the literal "opened it through
1629
+ // the tree" edge case this fallback was first built for) no longer
1630
+ // surfaces here either — activeListMode stays null, same as
1631
+ // sharedListHost staying closed. Acceptable: §12's own investigation
1632
+ // already established nothing in this codebase actually sets these flags
1633
+ // that way today: a `grep` for every direct setPanelVisible/openPanel
1634
+ // call against these two ids turned up only setActiveList/toggleList/
1635
+ // openAccountList, so this edge case was defensive, not real. What this
1636
+ // fallback still legitimately covers: a parent-tree state restore (e.g.
1637
+ // a persisted panel-tree snapshot opening ASSET_SELECT_LIST_PANEL +
1638
+ // NETWORK_LIST together on reload) racing ahead of this component's own
1639
+ // render on first mount.
1640
+ const renderAccountManagement = () =>
1641
+ // 2026-09-24, ACCOUNT_LIST_REWARDS_PANEL EXT wiring (minimal
1642
+ // bar, matching AGENT_HEADER_PANEL's own precedent) — swaps
1643
+ // the plain AccountListCard for the reward-aware shell so the
1644
+ // component is actually reachable in the extension. See
1645
+ // accountEntryToSpCoinAccount's own doc comment for the type
1646
+ // bridge; onPickAccount/onClaimRewards stay unwired (no real
1647
+ // on-chain reward-discovery feed exists in the extension yet
1648
+ // — see docs/npmMigrationDesign.md's own dated entry on that
1649
+ // real, separately-scoped gap).
1650
+ // 2026-10-03, on request ("the web app has the correct list") —
1651
+ // this branch rendered AccountListRewardsPanel (the reward-aware
1652
+ // card list), while the web app's own header account pill opens
1653
+ // LOCAL_ACCOUNT_WALLET_LIST ("Active Account Selection": Merit
1654
+ // Wallet/MetaMask groups, ACTIVE badge, Add a Wallet/Account).
1655
+ // AccountListCard is this package's pixel-copied portable version
1656
+ // of exactly that screen (see its header comment), so both apps
1657
+ // now show the same list from the same account groups.
1658
+ React.createElement(AssetListSelectPanel, {
1659
+ containerType: SP_COIN_DISPLAY.ASSET_SELECT_LIST_PANEL,
1660
+ // WALLET_ACCOUNTS is the closest real match — its own doc
1661
+ // comment in enums.ts: "Local wallet account list (all known
1662
+ // accounts)... Used by the Send flow's recipient picker."
1663
+ feedType: FEED_TYPE.WALLET_ACCOUNTS,
1664
+ listContent: React.createElement(AccountListCard, {
1665
+ remoteSelection,
1666
+ // 2026-11-06 (owner report: the selector offered the extension's own accounts and MetaMask's to the extension): another wallet is asking for THIS wallet's accounts, so only this wallet's own group is offered
1667
+ groups: (remoteSelection ? effectiveAccountGroups.filter((group) => group.id === pillWalletId || group.id === nativeWalletId || (nativeWalletId === 'serverKeystore' && group.id === 'hardhat')) : effectiveAccountGroups).map((group) => ({
1668
+ ...group,
1669
+ accounts: group.accounts.map((account) => ({
1670
+ ...account,
1671
+ // 2026-10-26 (owner report: "the ADDRESS_PANEL should not close
1672
+ // when I select another active Account for approval on this
1673
+ // page") -- this is the Active Account Selection list
1674
+ // (WALLET_ACCOUNTS feed, no address bar of its own), so "it"
1675
+ // means this list itself: it used to close on any successful
1676
+ // connect, same as every other list's pick-and-close row. That
1677
+ // was wrong here specifically -- connecting a different account
1678
+ // can kick off an approval flow (the wallet's own popup, a
1679
+ // pending-signature state) the owner needs to keep watching
1680
+ // this list to see settle, not have it yanked away the instant
1681
+ // the connect call returns. A failed connect already stayed
1682
+ // open (so its banner is seen); now a successful one does too.
1683
+ // The back arrow/X still close it explicitly.
1684
+ //
1685
+ // 2026-10-29 (owner report, live: "it should also close this
1686
+ // page to return to Add Sponsorship... the Recipient [list] can
1687
+ // be used as an example how Active Account should close its
1688
+ // panel") -- narrowed rather than reverting 2026-10-26 above: a
1689
+ // FAILED connect (result === false; ApproverController.connect's
1690
+ // own contract, see approverAccounts.ts) still leaves the list
1691
+ // open so its banner is seen, unchanged. A SUCCESSFUL connect
1692
+ // (no pending approval state to keep watching once it resolves
1693
+ // true) now closes again, matching every other pick-and-close
1694
+ // row in this app -- the Recipient list included, which already
1695
+ // worked this way and is what the owner pointed to as correct.
1696
+ onSelect: () => {
1697
+ if (remoteSelection) {
1698
+ if (account.address)
1699
+ remoteSelection.onToggle(account.address);
1700
+ return;
1701
+ }
1702
+ // 2026-11-06 (owner rule): a row the person has DISCONNECTED (kept in the list) is not an account to use until it is connected again
1703
+ if (account.connection && !account.connection.connected)
1704
+ return;
1705
+ // 2026-11-02 (owner request, Active Account Selection): picking an account makes it the active account and LEAVES the list open; the person closes it with Back. This reverses 2026-10-29 above (a successful pick closed the list).
1706
+ // 2026-11-12 (owner: "after I select a new activeAccount with leftClick, I should close this panel the same way back would do it by popping the stack"): this reverses 2026-11-02 above.
1707
+ // A pick that WORKED (the host answers true, or nothing) closes the list exactly as Back does (setActiveList(null) pops it); a pick the host refused (false: a failed connect) leaves it open so its banner is seen.
1708
+ void Promise.resolve(onAccountRowSelect?.(account.id, group.id)).then((picked) => {
1709
+ if (picked !== false)
1710
+ setActiveList(null);
1711
+ });
1712
+ },
1713
+ onInfoClick: account.address
1318
1714
  ? () => {
1319
- openTokenDetail(row.address);
1320
- onTokenIconClick?.(row.address);
1715
+ openAccountDetail(account.address, group.id);
1716
+ onAccountIconClick?.(account.address);
1321
1717
  }
1322
1718
  : undefined,
1323
1719
  })),
1324
- metaLabel: tokenRowsLoading ? 'Token Meta (loading...)' : 'Token Meta',
1325
- }),
1326
- })
1327
- : activeListMode === 'sendRecipient'
1328
- ? React.createElement(AssetListSelectPanel, {
1329
- containerType: SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL,
1330
- // sendRecipient's own real id (REMOTE_ACCOUNT_SEND_LIST) has a matching FEED_TYPE member exactly.
1331
- feedType: FEED_TYPE.REMOTE_ACCOUNT_SEND_LIST,
1332
- addressBar: buildAddressBar(recipientListRows, 'account', (address) => {
1333
- openAccountDetail(address);
1334
- onAccountIconClick?.(address);
1335
- }),
1336
- listContent: React.createElement(AssetListTable, {
1337
- // Prefers the caller's own real recipient directory
1338
- // (recipientRows — see that prop's own doc comment) once
1339
- // supplied; falls back to the wallet's own accounts
1340
- // (flatAccountRows) only for a consumer with no real
1341
- // recipient feed of its own yet, same "placeholder until a
1342
- // real feed exists" treatment every other fallback in this
1343
- // file already has.
1344
- // Send lists every known account (the web's REMOTE_ACCOUNT_SEND_LIST).
1345
- rows: recipientListRows.map((row) => ({
1346
- ...row,
1347
- onSelect: () => commitSelection(row),
1348
- infoIconSrc,
1349
- onInfoClick: row.address
1350
- ? () => {
1351
- openAccountDetail(row.address);
1352
- onAccountIconClick?.(row.address);
1353
- }
1354
- : undefined,
1355
- })),
1356
- metaLabel: 'Account Meta',
1357
- }),
1358
- })
1359
- : activeListMode === 'account'
1360
- ? (() => {
1361
- // 2026-09-24, ACCOUNT_LIST_REWARDS_PANEL EXT wiring (minimal
1362
- // bar, matching AGENT_HEADER_PANEL's own precedent) — swaps
1363
- // the plain AccountListCard for the reward-aware shell so the
1364
- // component is actually reachable in the extension. See
1365
- // accountEntryToSpCoinAccount's own doc comment for the type
1366
- // bridge; onPickAccount/onClaimRewards stay unwired (no real
1367
- // on-chain reward-discovery feed exists in the extension yet
1368
- // — see docs/npmMigrationDesign.md's own dated entry on that
1369
- // real, separately-scoped gap).
1370
- // 2026-10-03, on request ("the web app has the correct list") —
1371
- // this branch rendered AccountListRewardsPanel (the reward-aware
1372
- // card list), while the web app's own header account pill opens
1373
- // LOCAL_ACCOUNT_WALLET_LIST ("Active Account Selection": Merit
1374
- // Wallet/MetaMask groups, ACTIVE badge, Add a Wallet/Account).
1375
- // AccountListCard is this package's pixel-copied portable version
1376
- // of exactly that screen (see its header comment), so both apps
1377
- // now show the same list from the same account groups.
1378
- return React.createElement(AssetListSelectPanel, {
1379
- containerType: SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL,
1380
- // WALLET_ACCOUNTS is the closest real match — its own doc
1381
- // comment in enums.ts: "Local wallet account list (all known
1382
- // accounts)... Used by the Send flow's recipient picker."
1383
- feedType: FEED_TYPE.WALLET_ACCOUNTS,
1384
- listContent: React.createElement(AccountListCard, {
1385
- groups: effectiveAccountGroups.map((group) => ({
1386
- ...group,
1387
- accounts: group.accounts.map((account) => ({
1388
- ...account,
1389
- onSelect: () => {
1390
- // a connect that fails keeps the list open so its banner (accountListNotice) is seen; anything else closes it as before
1391
- const result = onAccountRowSelect?.(account.id, group.id);
1392
- if (result instanceof Promise)
1393
- void result.then((ok) => { if (ok !== false)
1394
- closeListOverlay(); });
1395
- else if (result === false)
1396
- return;
1397
- else
1398
- closeListOverlay();
1399
- },
1400
- onInfoClick: account.address
1401
- ? () => {
1402
- openAccountDetail(account.address);
1403
- onAccountIconClick?.(account.address);
1404
- }
1405
- : undefined,
1406
- })),
1407
- })),
1408
- infoIconSrc,
1409
- errorMessage: accountListNotice,
1410
- onAddWalletAccount: addAccountHost ? () => {
1411
- closeListOverlay();
1412
- openPanel(SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT, 'MeritWallet:openAddWalletAccount');
1413
- } : onAddAccount ? () => { closeListOverlay(); onAddAccount(); } : undefined,
1414
- }),
1415
- });
1416
- })()
1417
- : activeListMode === 'network'
1418
- ? React.createElement(AssetListSelectPanel, {
1419
- containerType: SP_COIN_DISPLAY.ASSET_LIST_SELECT_PANEL,
1420
- // No real FEED_TYPE concept exists for a network list — this
1421
- // enum has no NETWORK_LIST member. WALLET_ACCOUNTS reused as
1422
- // a safe inert placeholder: it's not a MANAGE_* value, so
1423
- // isManageView stays false regardless (same as every other
1424
- // branch here), the only thing feedType actually gates.
1425
- feedType: FEED_TYPE.WALLET_ACCOUNTS,
1426
- listContent: React.createElement(NetworkListTable, {
1427
- rows: visibleNetworkRows.map((row) => ({
1428
- ...row,
1429
- // NetworkListRow's own AssetSelectDropDown gates its whole
1430
- // symbol/name display on `hasEntity={!!address}` (a
1431
- // TokenListRow/AccountListRow-ism this row shape
1432
- // inherited). Corrected 2026-09-16: this used to fabricate
1433
- // a fake native-currency placeholder address to satisfy
1434
- // that gate — wrong, per the real app's own
1435
- // NetworkSelectDropDown.tsx (`address={isRow ?
1436
- // \`NetworkId : ${numericCurrentId}\` : triggerLabel}`),
1437
- // which feeds a literal "NetworkId : $id" label into this
1438
- // exact slot instead of a real/fake address. Matches that
1439
- // exactly now — real per-chain data, not a fabricated
1440
- // stand-in.
1441
- address: `NetworkId : ${row.id}`,
1442
- icon: row.iconSrc
1443
- ? React.createElement('img', {
1444
- src: row.iconSrc,
1445
- alt: '',
1446
- style: { width: '100%', height: '100%', objectFit: 'contain' },
1447
- })
1448
- : undefined,
1449
- onSelect: () => {
1450
- closeListOverlay();
1451
- onNetworkRowSelect?.(row.id);
1452
- },
1453
- // 2026-09-17, on live report ("selecting the network.png in
1454
- // any row is supposed to return the network... but instead
1455
- // opens NETWORK_PANEL for every row") — was wired to
1456
- // setNetworkDetailId (open the detail panel), on the belief
1457
- // this row's own right-side auth-toggle slot leaves no room
1458
- // for a separate info button, so the icon should fill that
1459
- // role instead (see NetworkListRow.tsx's own doc comment).
1460
- // That reasoning didn't match how every OTHER list row in
1461
- // this file actually behaves: Token/Recipient/Account rows
1462
- // never wire onIconClick as a detail-opener at the list
1463
- // level at all (only their TRIGGER pill's own icon does,
1464
- // once something is already active — see
1465
- // activeNetworkRow's onIconClick below) — a list row's
1466
- // icon commits, same as clicking the rest of the row, with
1467
- // no onIconClick override needed since AssetSelectDropDown
1468
- // already falls the click through to onRowClick when
1469
- // onIconClick is omitted. Simply not overriding it here
1470
- // makes network rows consistent with every other list.
1471
- })),
1472
- showTestNets,
1473
- onToggleShowTestNets: () => setShowTestNets((prev) => !prev),
1474
- }),
1720
+ })),
1721
+ infoIconSrc,
1722
+ errorMessage: accountListNotice,
1723
+ // 2026-11-02 (owner: Back from Add Wallet Account must step back to the list): the list is NOT closed here. Opening the add flow closes nothing; the flow is drawn over the list, and Back closes just the flow, so the list is what is left.
1724
+ onAddWalletAccount: addAccountHost ? () => {
1725
+ // 2026-11-03 (owner request): "Manage Wallets and Accounts" opens the wallet list (Select Software Wallet). 2026-11-05 (owner): the wallet list is a child of LOCAL_ACCOUNT_WALLET_LIST, not of the Add menu, so the Add menu is not
1726
+ // opened under it and Back lands on the account list.
1727
+ openPanel(SP_COIN_DISPLAY.WALLET_LIST_SELECT_PANEL, 'MeritWallet:openAddWalletAccount');
1728
+ } : onAddAccount ? () => { closeListOverlay(); onAddAccount(); } : undefined,
1729
+ }),
1730
+ });
1731
+ const renderNetworkList = () => React.createElement(AssetListSelectPanel, {
1732
+ containerType: SP_COIN_DISPLAY.ASSET_SELECT_LIST_PANEL,
1733
+ // No real FEED_TYPE concept exists for a network list — this
1734
+ // enum has no NETWORK_LIST member. WALLET_ACCOUNTS reused as
1735
+ // a safe inert placeholder: it's not a MANAGE_* value, so
1736
+ // isManageView stays false regardless (same as every other
1737
+ // branch here), the only thing feedType actually gates.
1738
+ feedType: FEED_TYPE.WALLET_ACCOUNTS,
1739
+ listContent: React.createElement(NetworkListTable, {
1740
+ rows: visibleNetworkRows.map((row) => ({
1741
+ ...row,
1742
+ // NetworkListRow's own AssetSelectDropDown gates its whole
1743
+ // symbol/name display on `hasEntity={!!address}` (a
1744
+ // TokenListRow/AccountListRow-ism this row shape
1745
+ // inherited). Corrected 2026-09-16: this used to fabricate
1746
+ // a fake native-currency placeholder address to satisfy
1747
+ // that gate — wrong, per the real app's own
1748
+ // NetworkSelectDropDown.tsx (`address={isRow ?
1749
+ // \`NetworkId : ${numericCurrentId}\` : triggerLabel}`),
1750
+ // which feeds a literal "NetworkId : $id" label into this
1751
+ // exact slot instead of a real/fake address. Matches that
1752
+ // exactly now — real per-chain data, not a fabricated
1753
+ // stand-in.
1754
+ address: `NetworkId : ${row.id}`,
1755
+ icon: row.iconSrc
1756
+ ? React.createElement('img', {
1757
+ src: row.iconSrc,
1758
+ alt: '',
1759
+ style: fitImageStyle,
1475
1760
  })
1476
- : null;
1761
+ : undefined,
1762
+ onSelect: () => {
1763
+ closeListOverlay();
1764
+ onNetworkRowSelect?.(row.id);
1765
+ },
1766
+ // 2026-09-17, on live report ("selecting the network.png in
1767
+ // any row is supposed to return the network... but instead
1768
+ // opens NETWORK_PANEL for every row") — was wired to
1769
+ // setNetworkDetailId (open the detail panel), on the belief
1770
+ // this row's own right-side auth-toggle slot leaves no room
1771
+ // for a separate info button, so the icon should fill that
1772
+ // role instead (see NetworkListRow.tsx's own doc comment).
1773
+ // That reasoning didn't match how every OTHER list row in
1774
+ // this file actually behaves: Token/Recipient/Account rows
1775
+ // never wire onIconClick as a detail-opener at the list
1776
+ // level at all (only their TRIGGER pill's own icon does,
1777
+ // once something is already active — see
1778
+ // activeNetworkRow's onIconClick below) — a list row's
1779
+ // icon commits, same as clicking the rest of the row, with
1780
+ // no onIconClick override needed since AssetSelectDropDown
1781
+ // already falls the click through to onRowClick when
1782
+ // onIconClick is omitted. Simply not overriding it here
1783
+ // makes network rows consistent with every other list.
1784
+ })),
1785
+ showTestNets,
1786
+ onToggleShowTestNets: () => setShowTestNets((prev) => !prev),
1787
+ }),
1788
+ });
1789
+ // Declared here, not near activeChild above, because it needs
1790
+ // renderNetworkList/renderAccountManagement (just defined) in scope —
1791
+ // see that earlier declaration site's own doc comment. activeChild itself
1792
+ // was already computed early (so activeListMode could derive from the
1793
+ // same single read) — passed straight through as AssetSelectListPanel's
1794
+ // one parameter, not re-read here.
1795
+ const sharedListHost = React.createElement(AssetSelectListPanelHost, {
1796
+ activeChild,
1797
+ walletChrome: true,
1798
+ renderNetworkList,
1799
+ renderAccountManagement,
1800
+ });
1801
+ const listOverlay = activeListMode === 'account'
1802
+ ? renderAccountManagement()
1803
+ : activeListMode === 'network'
1804
+ ? renderNetworkList()
1805
+ : null;
1477
1806
  // PanelGate's props type requires `children` as a prop, so createElement's children argument does not type-check here
1478
1807
  // eslint-disable-next-line react/no-children-prop
1479
- const gatedTabBody = React.createElement(PanelGate, { panel: TAB_PANEL_IDS[activeTab], children: tabBody });
1808
+ // While a step popup is drawn in MESSAGE_PANEL the tab body stays mounted (hidden): the popup's flow (the Sponsor tab's confirm hook) lives in it.
1809
+ const gatedTabBody = React.createElement(PanelGate, { panel: TAB_PANEL_IDS[activeTab], lazyLoad: !stepContentUp, children: tabBody });
1480
1810
  // 2026-10-10: the shared Add a Wallet/Account flow (the host's addAccountHost), shown over the wallet like the profile editor.
1811
+ // docs/meritMigration.txt row 19 (found by the deep walk, 2026-10-20, in BOTH apps): backing out of the Add a Wallet/Account screens left the tab's title over a BLANK body. "Add a Wallet" closes the account-list overlay and then opens the add flow, so when the flow is
1812
+ // popped there is no invoker for the panel tree to restore and no radio panel is visible; clicking the tab you are already on does nothing by rule (handleTabClick), so the blank stayed until another tab was chosen.
1813
+ // When the add flow has CLOSED (checked after the pop is applied: calling openPanel in the same tick as the pop raced with it and made the first tap on Go back do nothing), the active tab's own panel is shown again.
1814
+ const prevAddFlowTopRef = useRef(addFlowTop);
1815
+ useEffect(() => {
1816
+ const wasOpen = prevAddFlowTopRef.current != null;
1817
+ prevAddFlowTopRef.current = addFlowTop;
1818
+ // 2026-11-02: only when no list is open underneath: a list left open is what Back returns to, and a tab opened over it would hide it
1819
+ if (wasOpen && addFlowTop == null && activeChild == null)
1820
+ openPanel(TAB_PANEL_IDS[activeTab], 'MeritWallet:addFlowRestoreTab');
1821
+ }, [addFlowTop]);
1481
1822
  const addAccountOverlay = addFlowTop != null && addAccountHost
1482
1823
  ? React.createElement(AddAccountFlow, { host: addAccountHost, wallet, activeAddress: activeAccountAddress, nativeWalletId, walletConnection, onDisconnectWallet, onDone: () => closePanel(addFlowTop, 'MeritWallet:addAccountFlow:done') })
1483
1824
  : null;
1484
1825
  // docs/meritMigration.txt row 14: the one account detail screen (avatar, profile table, Private Key row); the host's accountDetailHost says how a save is signed and how a key is revealed.
1485
- const accountDetailOverlay = accountDetailVisible && accountDetailAddress && accountDetailHost
1486
- ? React.createElement(AccountDetailScreen, {
1487
- account: { address: accountDetailAddress, ...(accountDetail?.address === accountDetailAddress ? { name: accountDetail.name, symbol: accountDetail.symbol, logoURL: accountDetail.avatarSrc } : {}) },
1488
- role: SP_COIN_DISPLAY.ACTIVE_ACCOUNT,
1489
- host: accountDetailHost,
1490
- })
1491
- : null;
1492
- const tokenDetailOverlay = tokenDetailVisible && tokenDetailAddress
1493
- ? React.createElement(TokenDetailPanel, {
1494
- address: tokenDetailAddress,
1495
- // Same "only trust it once it answers the current address" gate
1496
- // as accountDetailOverlay above.
1497
- ...(tokenDetail?.address === tokenDetailAddress
1498
- ? {
1499
- logoSrc: tokenDetail.logoSrc,
1500
- name: tokenDetail.name,
1501
- symbol: tokenDetail.symbol,
1502
- decimals: tokenDetail.decimals,
1503
- website: tokenDetail.website,
1504
- explorer: tokenDetail.explorer,
1505
- description: tokenDetail.description,
1506
- loading: false,
1826
+ const roleAccounts = roleContext?.apiCoreSyncedMembers?.accounts;
1827
+ const roleDetail = agentPanelVisible
1828
+ ? { role: SP_COIN_DISPLAY.AGENT_ACCOUNT, account: roleAccounts?.agentAccount, empty: 'No agent selected.' }
1829
+ : sponsorPanelVisible
1830
+ ? { role: SP_COIN_DISPLAY.SPONSOR_ACCOUNT, account: roleAccounts?.sponsorAccount, empty: 'No sponsor selected.' }
1831
+ : recipientPanelVisible
1832
+ ? { role: SP_COIN_DISPLAY.RECIPIENT_ACCOUNT, account: roleAccounts?.recipientAccount, empty: 'No recipient selected.' }
1833
+ : undefined;
1834
+ // The active account's detail: the address picked from a list, else the active account (the panel opened from the header).
1835
+ const detailAddress = accountDetailAddress ?? String(activeAccountForDetail?.address ?? '').trim() ?? undefined;
1836
+ // 2026-11-08 (docs/design/removeAccountPlan.txt rows 3 and 5): the screen asks who holds the account (the wallet answers) and leaves when the account was removed
1837
+ const detailHost = accountDetailHost && wallet
1838
+ ? {
1839
+ ...accountDetailHost,
1840
+ accountSource: accountDetailHost.accountSource ?? (async (address, symbol) => {
1841
+ const value = await resolveAccountSource(wallet, address, activeAccountAddress, accountDetailGroup ?? undefined, symbol);
1842
+ setAccountDetailTitle(value?.title ?? null);
1843
+ return value;
1844
+ }),
1845
+ accountBalance: accountDetailHost.accountBalance ?? (fetchBalance
1846
+ ? async (address) => {
1847
+ const value = await fetchBalance({ accountAddress: address }).catch(() => undefined);
1848
+ // 2026-11-11: the amount only; the confirmation words it where it shows it (RemoveAccountPanel)
1849
+ return value && value > 0n ? formatUnits(value, 18) : undefined;
1507
1850
  }
1508
- : { loading: true }),
1509
- })
1510
- : null;
1851
+ : undefined),
1852
+ onAccountRemoved: (address, message) => {
1853
+ // 2026-11-10 (docs/design/removeAccountPlan.txt row 9): the removal is traced and confirmed in MESSAGE_PANEL; the lists re-read when the wallet announces the change
1854
+ engineTrace('account:removed', { address, message });
1855
+ if (message)
1856
+ accountNoticeStore.publish(message, [address]);
1857
+ setAccountDetailAddress(null);
1858
+ // 2026-11-11 (owner): by name, never a pop of whatever is on top: the stack held MESSAGE_PANEL and ACCOUNT_PANEL, and a single pop left ACCOUNT_PANEL listed under the list we landed on
1859
+ closePanel(SP_COIN_DISPLAY.MESSAGE_PANEL, 'NAV_CLOSE:MeritWallet:accountRemoved');
1860
+ closePanel(SP_COIN_DISPLAY.ACCOUNT_PANEL, 'NAV_CLOSE:MeritWallet:accountRemoved');
1861
+ },
1862
+ }
1863
+ : accountDetailHost;
1864
+ const accountDetailOverlay = !detailHost
1865
+ ? null
1866
+ : roleDetail
1867
+ ? roleDetail.account
1868
+ ? React.createElement(AccountDetailScreen, { account: roleDetail.account, role: roleDetail.role, host: detailHost })
1869
+ : React.createElement('div', { style: walletFrameStyle.roleEmpty }, roleDetail.empty)
1870
+ : accountDetailVisible && detailAddress
1871
+ ? React.createElement(AccountDetailScreen, {
1872
+ account: { address: detailAddress, ...(accountDetail?.address === detailAddress ? { name: accountDetail.name, symbol: accountDetail.symbol, logoURL: accountDetail.avatarSrc } : {}) },
1873
+ role: SP_COIN_DISPLAY.ACTIVE_ACCOUNT,
1874
+ host: detailHost,
1875
+ })
1876
+ // 2026-11-09 (owner: a panel open in the tree must not be blank): ACCOUNT_PANEL with no address to show (nothing picked, no active account) says so, as the role panels do
1877
+ : accountDetailVisible
1878
+ ? React.createElement('div', { style: walletFrameStyle.roleEmpty }, 'No account selected.')
1879
+ : null;
1880
+ // 2026-10-26 (owner report: TOKEN_PANEL's own TOKEN_LOGO/TOKEN_META_DATA
1881
+ // children never actually opened or closed anything — unlike
1882
+ // ACCOUNT_PANEL's AccountDetailScreen, which reads usePanelVisible on its
1883
+ // own ACCOUNT_LOGO/ACCOUNT_META_DATA pair). Root cause: this overlay used
1884
+ // to build the flat, static TokenDetailPanel (address/name/symbol rows,
1885
+ // no panel-tree awareness at all) instead of TokenPanel.tsx — the
1886
+ // package's own panel-tree-aware token screen, which already reads
1887
+ // TOKEN_META_DATA/TOKEN_LOGO visibility exactly the way AccountDetailScreen
1888
+ // reads its own pair (see this file's own 2026-10-20 doc comment above
1889
+ // previewToken: "TokenPanel.tsx ... already correctly uses" it) but was
1890
+ // never actually wired in here. TokenPanel is self-contained (reads
1891
+ // previewToken/chain/baseUrl via the engine's own hooks, same as
1892
+ // AccountDetailScreen reads its host), so no props are needed.
1893
+ // 2026-11-09 (owner: TOKEN_PANEL open in the panel tree but empty on screen; the trace showed tokenDetailVisible true, tokenDetailOverlay false, previewTokenAddress null): it is drawn whenever it is visible, not only when a token is
1894
+ // being previewed. TokenPanel decides what to show: the previewed token, else the buy or sell token, else its own "No token contract selected" notice (it closes itself when it was not opened from the panel tree).
1895
+ const tokenDetailOverlay = tokenDetailVisible ? React.createElement(TokenPanel, {}) : null;
1511
1896
  // No loading branch needed — unlike accounts/tokens, this row's own data
1512
1897
  // (networkRowsSource) is already fully in memory the moment the icon is
1513
1898
  // clicked; see onNetworkIconClick's own doc comment on MeritWalletProps.
1514
- const networkDetailRow = networkDetailVisible && networkDetailId
1515
- ? networkRowsSource.find((row) => row.id === networkDetailId)
1516
- : undefined;
1517
- const networkDetailOverlay = networkDetailRow
1518
- ? React.createElement(NetworkDetailPanel, {
1519
- id: networkDetailRow.id,
1520
- logoSrc: networkDetailRow.iconSrc,
1521
- name: networkDetailRow.name,
1522
- symbol: networkDetailRow.symbol,
1523
- isTestnet: networkDetailRow.isTestnet,
1524
- })
1525
- : null;
1899
+ // 2026-11-09 (owner: opened from the panel tree, NETWORK_PANEL showed an empty body): the screen is the shared ConnectedNetworkPanel (the live network, or the previewed chain), drawn by the same base layout as the Account screen, and it shows whenever NETWORK_PANEL is visible, not only after a row click.
1900
+ const networkDetailOverlay = networkDetailVisible ? React.createElement(ConnectedNetworkPanel, {}) : null;
1526
1901
  // 2026-09-29, real bug fix — see passwordMode's own doc comment on
1527
1902
  // MeritWalletProps for the full root-cause. Highest priority in this
1528
1903
  // chain, deliberately: a locked wallet should block every other overlay
@@ -1534,32 +1909,129 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1534
1909
  // display AGENT_HEADER_PANEL") — shared by the password overlay below and the
1535
1910
  // agent header's render site, so both follow the one condition.
1536
1911
  const passwordPanelShowing = !!walletLocked || activeMainOverlay === SP_COIN_DISPLAY.PASSWORD_PANEL;
1537
- const passwordOverlay = passwordPanelShowing && recoveryPhrase
1538
- ? React.createElement(RecoveryPhraseScreen, { recoveryPhrase, onContinue: () => onRecoveryPhraseSaved?.(), autoHide: false })
1539
- : passwordPanelShowing
1540
- ? React.createElement(PasswordPanel, {
1541
- mode: passwordMode,
1542
- showTitle: false,
1543
- icon: passwordIcon,
1544
- errorText: passwordErrorText,
1545
- noticeText: passwordNoticeText,
1546
- onSubmit: onPasswordSubmit,
1547
- submitting: passwordSubmitting,
1548
- clearOnSubmit: true,
1549
- })
1550
- : null;
1912
+ const passwordOverlay = passwordPanelShowing && recoveryPhrase && securitySetup && setupStage === 'warning'
1913
+ ? React.createElement(SecurityWarningScreen, { onContinue: () => { setSetupStage('phrase'); onRecoveryPhraseSaved?.(); } })
1914
+ : passwordPanelShowing && recoveryPhrase && securitySetup && setupStage === 'security'
1915
+ ? React.createElement(SecuritySetupScreen, { ...securitySetup, onDone: () => setSetupStage('warning') })
1916
+ : passwordPanelShowing && recoveryPhrase
1917
+ ? React.createElement(RecoveryPhraseScreen, { recoveryPhrase, onContinue: () => { if (securitySetup)
1918
+ setSetupStage('security');
1919
+ else
1920
+ onRecoveryPhraseSaved?.(); }, autoHide: false })
1921
+ : passwordPanelShowing && pendingUnlock && securitySetup?.signIn
1922
+ ? React.createElement(UnlockSecondStep, {
1923
+ steps: pendingUnlock.steps,
1924
+ signIn: securitySetup.signIn,
1925
+ onPassed: () => { const held = pendingUnlock; setPendingUnlock(undefined); onPasswordSubmit?.(held.password, held.confirm); },
1926
+ onCancel: () => setPendingUnlock(undefined),
1927
+ })
1928
+ : passwordPanelShowing
1929
+ ? React.createElement(PasswordPanel, {
1930
+ mode: passwordMode,
1931
+ showTitle: false,
1932
+ icon: passwordIconSrc ? React.createElement('img', { src: passwordIconSrc, alt: 'Merit Wallet', style: lockLogoStyle }) : undefined,
1933
+ errorText: passwordErrorText,
1934
+ noticeText: passwordNoticeText,
1935
+ // 2026-11-13 (owner: sign-in options): on the unlock screen the steps switched on in Config -> Security (passkey, authenticator app) are asked for after the password and before the unlock goes ahead
1936
+ onSubmit: (password, confirm) => {
1937
+ const signIn = securitySetup?.signIn;
1938
+ const steps = signIn && passwordMode === 'unlock' ? requiredSignInSteps(signIn.options(), signIn.enrolled()) : undefined;
1939
+ if (steps && anySignInStep(steps))
1940
+ setPendingUnlock({ password, confirm, steps });
1941
+ else
1942
+ onPasswordSubmit?.(password, confirm);
1943
+ },
1944
+ submitting: passwordSubmitting,
1945
+ clearOnSubmit: true,
1946
+ })
1947
+ : null;
1551
1948
  const passwordOverlayWithReset = passwordOverlay && walletLocked && onResetWallet
1552
- ? React.createElement(React.Fragment, null, passwordOverlay, React.createElement('div', { style: { textAlign: 'center', padding: '8px 12px' } }, React.createElement('button', { type: 'button', onClick: onResetWallet, style: { background: 'none', border: 0, color: '#94a3b8', textDecoration: 'underline', cursor: 'pointer', font: 'inherit', fontSize: 12 } }, 'Forgot password? Reset wallet')))
1949
+ ? React.createElement(React.Fragment, null, passwordOverlay, React.createElement('div', { style: walletFrameStyle.forgotRow }, React.createElement('button', { type: 'button', onClick: onResetWallet, style: walletFrameStyle.forgotLink }, 'Forgot password? Reset wallet')))
1553
1950
  : passwordOverlay;
1554
1951
  // 2026-10-05 — PANEL_TREE_PANEL is a main-radio view (opened from Config), not a tab: it replaces the body
1555
1952
  // while it is the showing member, exactly like the other full-body overlays.
1556
1953
  const panelTreeOverlay = panelTreeVisible ? React.createElement(PanelTreePanel) : null;
1954
+ // 2026-10-23 — MERIT_INFO_PANEL is PANEL_TREE_PANEL's own sibling in the
1955
+ // same MAIN_RADIO_OVERLAY_PANELS radio group (the "Merit Wallet" header
1956
+ // title opens it, WalletHeader.tsx's handleTitleClick) — same "replaces
1957
+ // the body while showing" treatment. See meritInfoVisible's own doc
1958
+ // comment above for why this was missing.
1959
+ // 2026-10-24 (owner report: the extension's own About panel showed
1960
+ // "N/A" for every field, the web app's showed real data) — baseUrl was
1961
+ // left at its default ('', same-origin relative), exactly the
1962
+ // portability wrinkle MeritInfoPanelReal.tsx's own header comment
1963
+ // already flagged: a relative fetch of /assets/miscellaneous/
1964
+ // meritInfo.json resolves fine for the web app but not from the
1965
+ // extension's chrome-extension:// origin. walletAssetBaseUrl already
1966
+ // exists and is passed to WalletPanel/WalletSelectDropDownHosted below
1967
+ // for this exact reason ("a host on another origin, the extension,
1968
+ // passes the web app's address") — just never wired to this panel too.
1969
+ const meritInfoOverlay = meritInfoVisible
1970
+ ? React.createElement(MeritInfoPanelReal, { baseUrl: walletAssetBaseUrl })
1971
+ : null;
1557
1972
  // 2026-10-09 -- the result card of a swap (Success / error, from the shared runUniswapSwap flow). The web app shows MESSAGE_PANEL through its
1558
1973
  // overlayHost; a host without one (the extension) gets it here, in place of the body, until the user closes it.
1559
- const messageOverlay = messageVisible && !overlayHost ? React.createElement(ConnectedMessagePanel) : null;
1974
+ const messageOverlay = messageVisible ? React.createElement(ConnectedMessagePanel) : null;
1560
1975
  // 2026-10-13 (owner request): WALLET_PANEL, the active wallet's account.json. The web app shows it through its overlayHost (walletPanel slot); a host without one (the extension) gets it here, in place of the body.
1561
- const walletPanelOverlay = walletPanelVisible && !overlayHost ? React.createElement(WalletPanel, { walletId: activeWalletId, baseUrl: walletAssetBaseUrl, showLogo: walletLogoVisible, showMetaData: walletMetaDataVisible }) : null;
1562
- const body = messageOverlay ?? passwordOverlayWithReset ?? panelTreeOverlay ?? addAccountOverlay ?? accountDetailOverlay ?? tokenDetailOverlay ?? networkDetailOverlay ?? walletPanelOverlay ?? (listHost && panelTreeListOpen ? listHost : null) ?? listOverlay ?? gatedTabBody;
1976
+ const walletPanelOverlay = walletPanelVisible ? React.createElement(WalletPanel, { walletId: activeWalletId, baseUrl: walletAssetBaseUrl, showLogo: walletLogoVisible, showMetaData: walletMetaDataVisible }) : null;
1977
+ // 2026-10-23 — gated on `activeChild != null`, not the coarser
1978
+ // panelTreeListOpen (= ASSET_SELECT_LIST_PANEL's own flag alone): the two
1979
+ // can genuinely disagree (parent open, no child flag actually set is a
1980
+ // real transient state), and sharedListHost is a React element — always
1981
+ // truthy as a VALUE even when it will render null internally — so gating
1982
+ // on the less precise flag would let this `??` chain stop here with
1983
+ // nothing on screen instead of correctly falling through to listOverlay/
1984
+ // gatedTabBody.
1985
+ // 2026-10-26 (diagnostic, owner report: right-clicking a token row still
1986
+ // shows a blank body after tokenDetailOverlay was switched to read
1987
+ // previewToken) -- logs which of these resolved truthy/falsy right
1988
+ // before the chain picks one, so the next capture shows which overlay is
1989
+ // actually winning instead of tokenDetailOverlay.
1990
+ panelTreeTrace('MeritWallet:body overlay chain', {
1991
+ messageOverlay: messageOverlay != null,
1992
+ passwordOverlayWithReset: passwordOverlayWithReset != null,
1993
+ panelTreeOverlay: panelTreeOverlay != null,
1994
+ meritInfoOverlay: meritInfoOverlay != null,
1995
+ addAccountOverlay: addAccountOverlay != null,
1996
+ accountDetailOverlay: accountDetailOverlay != null,
1997
+ accountDetailVisible,
1998
+ tokenDetailOverlay: tokenDetailOverlay != null,
1999
+ tokenDetailVisible,
2000
+ previewTokenAddress,
2001
+ networkDetailOverlay: networkDetailOverlay != null,
2002
+ walletPanelOverlay: walletPanelOverlay != null,
2003
+ activeChild: activeChild != null ? SP_COIN_DISPLAY[activeChild] : null,
2004
+ listOverlay: listOverlay != null,
2005
+ });
2006
+ // the remote's account selector sits above every other open screen (add-wallet flow, wallet list, detail views) until Done / Cancel
2007
+ // the selector sits above whatever screens were open when it started (the wallet list is one of them); only an add / manage screen opened AFTER it started, from its own Manage button, goes above it
2008
+ const remoteStart = useRef({ menu: false });
2009
+ const remoteWasOpen = useRef(false);
2010
+ if (remoteSelection && !remoteWasOpen.current)
2011
+ remoteStart.current = { top: addFlowTop, menu: addMenuVisible };
2012
+ remoteWasOpen.current = !!remoteSelection;
2013
+ const addFlowOpenedSinceRemote = addFlowTop !== remoteStart.current.top || (addMenuVisible && !remoteStart.current.menu);
2014
+ const remoteOverlay = remoteSelection && !addFlowOpenedSinceRemote ? renderAccountManagement() : null;
2015
+ const validateRemoteVisible = usePanelVisible(SP_COIN_DISPLAY.VALIDATE_REMOTE_CONNECT);
2016
+ const approvalPending = !!approvalPage;
2017
+ useEffect(() => {
2018
+ if (approvalPending)
2019
+ openPanel(SP_COIN_DISPLAY.VALIDATE_REMOTE_CONNECT, 'MeritWallet:approvalPage:open');
2020
+ else
2021
+ closePanel(SP_COIN_DISPLAY.VALIDATE_REMOTE_CONNECT, 'MeritWallet:approvalPage:close');
2022
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2023
+ }, [approvalPending]);
2024
+ const approvalOverlay = approvalPage && validateRemoteVisible && !passwordOverlay ? approvalPage.element : null;
2025
+ const body = messageOverlay ?? passwordOverlayWithReset ?? approvalOverlay ?? remoteOverlay ?? panelTreeOverlay ?? meritInfoOverlay ?? addAccountOverlay ?? accountDetailOverlay ?? tokenDetailOverlay ?? networkDetailOverlay ?? walletPanelOverlay ?? (activeChild != null ? sharedListHost : null) ?? listOverlay ?? null;
2026
+ // The tab body always sits in the SAME slot of one Fragment ([overlay, tab body]): while a step popup is drawn in MESSAGE_PANEL it stays there (hidden) beside
2027
+ // the panel, so the popup's owner (the Sponsor trade button's confirm flow) is not unmounted by the panel opening. Switching between a bare element and a
2028
+ // Fragment instead would remount the whole tab.
2029
+ const showTabBody = body == null || (stepContentUp && body === messageOverlay);
2030
+ // the Rewards list opened by Unstake: it self-gates on SPONSOR_STAKING_LIST and is drawn inside WALLET_RADIO_PANELS (below the tab strip), skipped while the login page shows (the flag is remembered across sessions)
2031
+ const stakingListNode = !passwordPanelShowing
2032
+ ? (stakingHost ? React.createElement(ConnectedSponsorStakingList, { host: stakingHost }) : React.createElement(SponsorStakingListPanel, { recipients: [], stakeInfoByAddress: {}, decimals: 18, loading: false }))
2033
+ : null;
2034
+ const bodyWithTab = React.createElement(React.Fragment, null, body, showTabBody ? gatedTabBody : null);
1563
2035
  // Matches the real app's own per-tab titles (useActiveWalletPanelTitle.tsx,
1564
2036
  // its `sponsorshipPanelVisible`/`tradingTabVisible`/`sendTabVisible`/
1565
2037
  // `rewardsTabVisible` ternary) — PanelTitle's own default ("Trading
@@ -1574,132 +2046,162 @@ function MeritWalletInner({ resolveAssetAddress, docked = false, fullWidth = fal
1574
2046
  // wording. PasswordPanel's inner heading is suppressed here because this
1575
2047
  // component already shows the state in PanelTitle; passwordMode drives both.
1576
2048
  // 2026-10-15 (owner request): "Select Software Wallet" / "Select Hardware Wallet" / "Select Wallet", by the list's two footer boxes
1577
- const walletListScreenTitle = useWalletListTitle();
1578
- const panelTitle = passwordOverlay
1579
- ? passwordMode === 'setup' ? 'Create Account' : 'Account Login'
1580
- : walletListLeafVisible
1581
- ? walletListScreenTitle
1582
- : addMenuVisible && panelTreeListOpen
1583
- ? 'Add Wallet Account'
1584
- : panelTreeVisible
1585
- ? 'Panel Tree'
1586
- : accountDetailVisible
1587
- ? 'Account Details'
1588
- : tokenDetailVisible
1589
- ? 'Token Details'
1590
- : networkDetailVisible
1591
- ? 'Network Details'
1592
- : activeListMode === 'sellToken' ||
1593
- activeListMode === 'buyToken' ||
1594
- activeListMode === 'sendToken'
1595
- ? 'Select a Token'
1596
- : activeListMode === 'sendRecipient'
1597
- ? 'Select Recipient'
1598
- : activeListMode === 'account'
1599
- ? 'Active Account Selection'
1600
- : activeListMode === 'network'
1601
- ? 'Select Network'
1602
- : activeTab === 'SWAP'
1603
- ? 'Trading Station'
1604
- : activeTab === 'SEND'
1605
- ? 'Send Token'
1606
- : activeTab === 'SPONSOR'
1607
- ? 'Add Sponsorship'
1608
- : activeTab === 'REWARDS'
1609
- ? 'Rewards Management'
1610
- : 'Wallet Config';
1611
- return (_jsx(WalletBalanceContext.Provider, { value: walletBalanceValue, children: _jsx(WalletConnectProvider, { hasAccount: !!String(activeAccountAddress ?? '').trim(), openAccountList: () => setActiveList('account'), children: _jsx(ActiveAccountProfileContext.Provider, { value: activeAccountEntry ? { address: activeAccountEntry.address, name: activeAccountEntry.name, symbol: activeAccountEntry.symbol, logoURL: activeAccountEntry.iconSrc } : undefined, children: _jsxs("div", { style: {
1612
- position: 'relative',
1613
- display: 'flex',
1614
- flexDirection: 'column',
1615
- minHeight: 5,
1616
- width: fullWidth ? '100%' : 'min(364px, calc(100vw - 32px))',
1617
- overflow: 'hidden',
1618
- pointerEvents: 'auto',
1619
- border: '1px solid #2e3654',
1620
- background: walletColors.background,
1621
- color: walletColors.white,
1622
- boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
1623
- ...(docked
1624
- ? { height: '100%', borderRadius: 0, borderRight: 'none' }
1625
- : { maxHeight: 'min(1000px, calc(100vh - 100px))', borderRadius: 15 }),
1626
- }, children: [passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.LOGIN_HEADER, lazyLoad: false, children: _jsx(LoginHeader, { title: recoveryPhrase || passwordMode === 'setup' ? 'Create Merit Wallet Account' : 'Merit Wallet Login', onClose: onClose, appType: appType, wwwIconSrc: wwwIconSrc, closeIconSrc: closeIconSrc }) })), !passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.WALLET_NETWORK_HEADER, lazyLoad: false, children: _jsx(WalletNetworkHeader, { mode: "normal", leftSlot: _jsx(NetworkSelectDropDown, { label: activeNetworkRow?.name,
1627
- // 2026-09-16, on live report ("the NetworkSelectDropDown is
1628
- // not showing the network icon in the WALLET_NETWORK_HEADER")
1629
- // — activeNetworkRow.iconSrc was already real (same value the
1630
- // Select Network list's own rows resolve their icon from
1631
- // below), this render site just never turned it into an
1632
- // <img> and passed it into NetworkSelectDropDown's own icon
1633
- // slot — same conversion the network-list rows already do.
1634
- icon: activeNetworkRow?.iconSrc
1635
- ? React.createElement('img', {
1636
- src: activeNetworkRow.iconSrc,
1637
- alt: '',
1638
- style: { width: '100%', height: '100%', objectFit: 'contain' },
1639
- })
1640
- : undefined, onSelectClick: () => toggleList('network'), onIconClick: activeNetworkRow
1641
- ? () => {
1642
- openNetworkDetail(activeNetworkRow.id);
1643
- onNetworkIconClick?.(activeNetworkRow.id);
1644
- }
1645
- : undefined, chevronUp: activeListMode === 'network' }), titleBadgeSrc: titleBadgeSrc, onRefresh: onRefresh, refreshing: refreshing, onClose: onClose, appType: appType, wwwIconSrc: wwwIconSrc, closeIconSrc: closeIconSrc }) })), !passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.WALLET_ACCOUNT_HEADER, lazyLoad: false, children: _jsx(WalletAccountHeader, { icon: activeAccountEntry?.icon, address: activeAccountEntry?.address, symbol: activeAccountEntry?.symbol, name: activeAccountEntry?.name, placeholderLabel: "Select Account", onSelectClick: () => toggleList('account'), onIconClick: (address) => {
1646
- if (!address)
1647
- return;
1648
- openAccountDetail(address);
1649
- onAccountIconClick?.(address);
1650
- }, chevronUp: activeListMode === 'account', walletName: activeWalletName }) })), !passwordPanelShowing && (_jsx(AgentHeaderPanel, { agentName: agentAccount?.name, children: agentSelectSlot ?? _jsx(ConnectedAgentSelectDropDown, {}) })), _jsxs("div", { style: { display: 'flex', minHeight: 0, flex: 1, flexDirection: 'column', overflow: 'hidden' }, children: [!passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.PANEL_TITLE, lazyLoad: false, children: _jsx(PanelTitle, { title: panelTitle, onMenuClick: handleMenuClick, menuOpen: menuTabHeaderVisible,
1651
- // 2026-10-03, on request — on the Account Login / Create Account
1652
- // screen the title bar shows only its title: no back button (nothing
1653
- // to go back to while locked) and no hamburger menu.
1654
- hideBackButton: passwordPanelShowing, hideMenuButton: passwordPanelShowing,
1655
- // 2026-09-15 — the back arrow already existed (inert, no
1656
- // onBackClick ever passed); now real whenever a list overlay is
1657
- // open, closing it back to the tab it was opened from.
1658
- // 2026-10-05, task 11c (live report: back arrow did nothing
1659
- // with SPONSORSHIP_PANEL open — no detail overlay was open,
1660
- // so this was `undefined` and the click was inert) — final
1661
- // fallback is now the real panel-tree pop-top: closePanel's
1662
- // legacy string-first overload pops the top IS_STACK_COMPONENT
1663
- // off displayStack, hides it (NAV_CLOSE: invoker →
1664
- // radio-restore-on-pop) and re-opens whatever is next on the
1665
- // stack (usePanelTree.ts's closePanel pop-top branch). No-ops
1666
- // safely when the stack is empty. Same call shape as the web
1667
- // app's own ActiveWalletPanelTitle back button.
1668
- onBackClick: accountDetailVisible
1669
- ? () => {
1670
- setAccountDetailAddress(null);
1671
- closePanel('NAV_POP:MeritWallet:accountDetailBack');
1672
- }
1673
- : tokenDetailVisible
2049
+ const walletListScreenTitle = useWalletListTitle(nativeWalletId === 'localVault' ? 'Extension' : nativeWalletId === 'serverKeystore' ? 'Web App' : undefined);
2050
+ const panelTitle = messageVisible && confirmTitle
2051
+ ? confirmTitle
2052
+ : stepContentUp && stepTitle
2053
+ ? stepTitle
2054
+ : messageOwnTitle && !stepContentUp
2055
+ ? messageOwnTitle
2056
+ : passwordOverlay
2057
+ ? passwordMode === 'setup' ? 'Create Account' : 'Account Login'
2058
+ : approvalOverlay
2059
+ ? approvalPage.title
2060
+ : remoteSelection
2061
+ // 2026-11-07 (owner): "Add Merit <app> Account(s)", the app whose accounts the person is choosing from (this one: the extension's vault, or the web app)
2062
+ ? `Add Merit ${nativeWalletId === 'localVault' ? 'Extension' : 'Web App'} Account(s)`
2063
+ : walletListLeafVisible
2064
+ ? walletListScreenTitle
2065
+ : createAccountLeafVisible && addMenuVisible && panelTreeListOpen
2066
+ // 2026-11-07 (owner): Create a Merit Account is the Account Details screen in add mode
2067
+ ? 'Add New Account'
2068
+ : addMenuVisible && panelTreeListOpen
2069
+ ? `${pillWalletId ? walletName(pillWalletId) : 'Merit Wallet'} Manager`
2070
+ : panelTreeVisible
2071
+ ? 'Panel Tree'
2072
+ : meritInfoVisible
2073
+ ? 'About Merit Wallet'
2074
+ : accountDetailVisible
2075
+ ? accountDetailTitle ?? 'Account Details'
2076
+ : tokenDetailVisible
2077
+ ? 'Token Details'
2078
+ : networkDetailVisible
2079
+ ? 'Network Details'
2080
+ : activeListMode === 'sellToken' ||
2081
+ activeListMode === 'buyToken' ||
2082
+ activeListMode === 'sendToken'
2083
+ ? 'Select a Token'
2084
+ : activeListMode === 'sendRecipient'
2085
+ ? 'Select Recipient'
2086
+ : activeListMode === 'account'
2087
+ ? 'Active Account Selection'
2088
+ : activeListMode === 'network'
2089
+ ? 'Select Network'
2090
+ : activeTab === 'SWAP'
2091
+ ? 'Trading Station'
2092
+ : activeTab === 'SEND'
2093
+ ? 'Send Token'
2094
+ : activeTab === 'SPONSOR'
2095
+ ? 'Add Sponsorship'
2096
+ : activeTab === 'REWARDS'
2097
+ ? stakingListVisible
2098
+ ? stakingListMode.kind === 'claim' ? `Claim ${stakingListMode.role} Rewards` : 'Unstake Sponsor Rewards'
2099
+ : 'Rewards Management'
2100
+ : 'Wallet Config';
2101
+ // 2026-11-09 (owner design): the refresh tree of this wallet (refresh/refreshTree.ts); every screen's loaded data is a node in it
2102
+ const refreshTreeRef = useRef(undefined);
2103
+ if (!refreshTreeRef.current)
2104
+ refreshTreeRef.current = createRefreshTree();
2105
+ const refreshTree = refreshTreeRef.current;
2106
+ // 2026-11-09 (owner): the Refresh button stays disabled until the tree has finished, not only while the host's own step runs: the token changed and the tree has not reported done yet means busy (no gap between the two)
2107
+ const [treeDoneToken, setTreeDoneToken] = useState(refreshToken ?? 0);
2108
+ const imageCacheValue = useMemo(() => ({ caches: iconCachesRef.current, baseUrl: baseUrl ?? '' }), [baseUrl]);
2109
+ const refreshBusy = !!refreshing || (refreshToken ?? 0) !== treeDoneToken;
2110
+ return (_jsx(ImageCacheContext.Provider, { value: imageCacheValue, children: _jsx(RefreshTreeProvider, { tree: refreshTree, refreshToken: refreshToken ?? 0, onDone: setTreeDoneToken, children: _jsx(WalletBalanceContext.Provider, { value: walletBalanceValue, children: _jsx(WalletConnectProvider, { hasAccount: !!String(activeAccountAddress ?? '').trim(), openAccountList: () => setActiveList('account'), children: _jsx(ActiveAccountProfileContext.Provider, { value: activeAccountEntry ? { address: activeAccountEntry.address, name: activeAccountEntry.name, symbol: activeAccountEntry.symbol, logoURL: activeAccountEntry.iconSrc } : undefined, children: _jsxs("div", { className: walletFrameClassName, style: walletFrameStyle.frame(fullWidth, docked), children: [_jsx("style", { children: walletFrameCss }), passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.LOGIN_HEADER, lazyLoad: false, children: _jsx(LoginHeader, { title: recoveryPhrase || passwordMode === 'setup' ? 'Create Merit Wallet Account' : 'Merit Wallet Login', onClose: onClose, appType: appType, wwwIconSrc: wwwIconSrc, closeIconSrc: closeIconSrc }) })), !passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.WALLET_NETWORK_HEADER, lazyLoad: false, children: _jsx(WalletNetworkHeader, { mode: "normal", leftSlot: _jsx(NetworkSelectDropDown, { label: activeNetworkRow?.name, symbol: activeChainInfo?.symbol ?? activeNetworkRow?.symbol, name: activeChainInfo?.name ?? activeNetworkRow?.name, id: activeNetworkRow?.id,
2111
+ // 2026-09-16, on live report ("the NetworkSelectDropDown is
2112
+ // not showing the network icon in the WALLET_NETWORK_HEADER")
2113
+ // — activeNetworkRow.iconSrc was already real (same value the
2114
+ // Select Network list's own rows resolve their icon from
2115
+ // below), this render site just never turned it into an
2116
+ // <img> and passed it into NetworkSelectDropDown's own icon
2117
+ // slot — same conversion the network-list rows already do.
2118
+ icon: activeNetworkRow?.iconSrc
2119
+ ? React.createElement('img', {
2120
+ src: activeNetworkRow.iconSrc,
2121
+ alt: '',
2122
+ style: fitImageStyle,
2123
+ })
2124
+ : undefined, onSelectClick: () => toggleList('network'), onIconClick: activeNetworkRow
1674
2125
  ? () => {
1675
- setTokenDetailAddress(null);
1676
- closePanel('NAV_POP:MeritWallet:tokenDetailBack');
2126
+ openNetworkDetail(activeNetworkRow.id);
2127
+ onNetworkIconClick?.(activeNetworkRow.id);
1677
2128
  }
1678
- : networkDetailVisible
1679
- ? () => {
1680
- setNetworkDetailId(null);
1681
- closePanel('NAV_POP:MeritWallet:networkDetailBack');
1682
- }
1683
- : addFlowTop != null
1684
- ? () => closePanel(addFlowTop, 'NAV_POP:MeritWallet:addFlowBack')
1685
- : activeListMode
1686
- ? () => setActiveList(null)
1687
- : () => closePanel('MeritWallet:back') }) })), _jsxs(MenuTabHeaderBar, { open: menuTabHeaderVisible && !passwordPanelShowing, activeTab: activeTab, onTabClick: handleTabClick, children: [showWalletSelect && !passwordPanelShowing && panelTreeListOpen && (addMenuVisible || walletListVisible) && !walletListLeafVisible && (_jsx("div", { id: "WALLET_SELECT_ROW", style: { padding: '6px 10px', display: 'flex', alignItems: 'center' }, children: _jsx(WalletSelectDropDownHosted, { walletId: pillWalletId, displayName: pillWalletId ? walletName(pillWalletId) : undefined, baseUrl: walletAssetBaseUrl }) })), _jsx(WalletRadioPanels, { overlayHost: overlayHost, children: body })] })] }), _jsx(WalletSessionDialogs, { dialog: sessionDialog, onClose: () => setSessionDialog(null), session: walletSession }), !overlayHost && !passwordPanelShowing && (stakingHost ? _jsx(ConnectedSponsorStakingList, { host: stakingHost }) : _jsx(SponsorStakingListPanel, { recipients: [], stakeInfoByAddress: {}, decimals: 18, loading: false })), SHOW_BUILD_MARKERS && (_jsxs("div", { style: {
1688
- position: 'absolute',
1689
- bottom: 2,
1690
- left: 6,
1691
- zIndex: 999999,
1692
- font: '9px monospace',
1693
- color: walletColors.slateMid,
1694
- pointerEvents: 'none',
1695
- }, children: ["\u27E8@sponsorcoin/merit-wallet/MeritWallet.tsx \u00B7 build ", PACKAGE_BUILD, "\u27E9"] }))] }) }) }) }));
2129
+ : undefined, chevronUp: activeListMode === 'network' || activeChild === SP_COIN_DISPLAY.NETWORK_LIST }), titleBadgeSrc: titleBadgeSrc, extraActions: _jsx(DebugBugButton, {}), onRefresh: onRefresh, refreshing: refreshBusy, onClose: onClose, appType: appType, wwwIconSrc: wwwIconSrc, closeIconSrc: closeIconSrc }) })), !passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.WALLET_ACCOUNT_HEADER, lazyLoad: false, children: _jsx(WalletAccountHeader, { icon: activeAccountEntry?.icon, address: activeAccountEntry?.address, symbol: activeAccountEntry?.symbol, name: activeAccountEntry?.name, placeholderLabel: "Select Account", onSelectClick: () => toggleList('account'), onIconClick: (address) => {
2130
+ if (!address)
2131
+ return;
2132
+ openAccountDetail(address);
2133
+ onAccountIconClick?.(address);
2134
+ },
2135
+ // 2026-11-11 (owner): a right click on the avatar opens ACCOUNT_PANEL too, through AccountSelectDropDown
2136
+ onIconContextMenu: (address) => { if (address)
2137
+ openAccountDetail(address); }, walletName: activeWalletName }) })), !passwordPanelShowing && (_jsx(AgentHeaderPanel, { agentName: agentAccount?.name, children: _jsx(ConnectedAgentSelectDropDown, {}) })), _jsxs("div", { style: walletFrameStyle.body, children: [!passwordPanelShowing && (_jsx(PanelGate, { panel: SP_COIN_DISPLAY.PANEL_TITLE, lazyLoad: false, children: _jsx(PanelTitle, { title: panelTitle, onMenuClick: handleMenuClick, menuOpen: menuTabHeaderVisible,
2138
+ // 2026-10-03, on request — on the Account Login / Create Account
2139
+ // screen the title bar shows only its title: no back button (nothing
2140
+ // to go back to while locked) and no hamburger menu.
2141
+ hideBackButton: passwordPanelShowing, hideMenuButton: passwordPanelShowing,
2142
+ // 2026-09-15 — the back arrow already existed (inert, no
2143
+ // onBackClick ever passed); now real whenever a list overlay is
2144
+ // open, closing it back to the tab it was opened from.
2145
+ // 2026-10-05, task 11c (live report: back arrow did nothing
2146
+ // with SPONSORSHIP_PANEL open — no detail overlay was open,
2147
+ // so this was `undefined` and the click was inert) — final
2148
+ // fallback is now the real panel-tree pop-top: closePanel's
2149
+ // legacy string-first overload pops the top IS_STACK_COMPONENT
2150
+ // off displayStack, hides it (NAV_CLOSE: invoker →
2151
+ // radio-restore-on-pop) and re-opens whatever is next on the
2152
+ // stack (usePanelTree.ts's closePanel pop-top branch). No-ops
2153
+ // safely when the stack is empty. Same call shape as the web
2154
+ // app's own ActiveWalletPanelTitle back button.
2155
+ onBackClick: approvalOverlay
2156
+ ? approvalPage.onCancel
2157
+ : remoteSelection
2158
+ ? remoteSelection.onCancel
2159
+ : accountDetailVisible
2160
+ ? () => {
2161
+ setAccountDetailAddress(null);
2162
+ closePanel('NAV_POP:MeritWallet:accountDetailBack');
2163
+ }
2164
+ : tokenDetailVisible
2165
+ ? () => {
2166
+ if (previewToken)
2167
+ setPreviewTokenContract(undefined);
2168
+ closePanel('NAV_POP:MeritWallet:tokenDetailBack');
2169
+ }
2170
+ : networkDetailVisible
2171
+ ? () => {
2172
+ setNetworkDetailId(null);
2173
+ closePanel('NAV_POP:MeritWallet:networkDetailBack');
2174
+ }
2175
+ : addFlowTop != null
2176
+ ? () => closePanel(addFlowTop, 'NAV_POP:MeritWallet:addFlowBack')
2177
+ // 2026-10-28, real bug fix (live report, Test page:
2178
+ // "the down chevron points up" after Back from the
2179
+ // Sponsor tab's recipient list, and never reverts) —
2180
+ // activeListMode is null for a list opened directly
2181
+ // through openActiveListPanel rather than this
2182
+ // component's own setActiveList (RecipientSelectDropDown.tsx's
2183
+ // REMOTE_ACCOUNT_RECIPIENT_LIST among them — it only ever
2184
+ // recognizes its own 'account'/'network' fallback modes,
2185
+ // see this file's own activeListMode derivation comment),
2186
+ // so Back fell through to the generic stack-pop below,
2187
+ // which never calls closeActiveListPanel — the list's
2188
+ // flags stayed stuck true, and the chevron (which reads
2189
+ // REMOTE_ACCOUNT_RECIPIENT_LIST's own visibility) stayed
2190
+ // pointing up forever. activeChild (useActiveListPanelChild,
2191
+ // already this file's own single source of truth
2192
+ // elsewhere) covers all six ACTIVE_LIST_PANEL_MODES
2193
+ // members, not just two — same close action already
2194
+ // proven correct for account/network.
2195
+ : activeListMode || activeChild != null
2196
+ ? () => setActiveList(null)
2197
+ : () => closePanel('MeritWallet:back') }) })), _jsxs(MenuTabHeaderBar, { open: menuTabHeaderVisible && !passwordPanelShowing, activeTab: highlightedTab, onTabClick: handleTabClick, children: [showWalletSelect && !remoteSelection && !passwordPanelShowing && panelTreeListOpen && walletListVisible && !addMenuVisible && !walletListLeafVisible && (_jsx("div", { id: "WALLET_SELECT_ROW", style: walletFrameStyle.walletSelectRow, children: _jsx(WalletSelectDropDownHosted, { walletId: pillWalletId, displayName: pillWalletId ? walletName(pillWalletId) : undefined, baseUrl: walletAssetBaseUrl }) })), _jsxs(WalletRadioPanels, { children: [bodyWithTab, stakingListNode] })] })] }), _jsx(WalletSessionDialogs, { dialog: sessionDialog, onClose: () => setSessionDialog(null), session: walletSession })] }) }) }) }) }) }));
1696
2198
  }
1697
2199
  let warnedNoAccountSource = false;
1698
2200
  /**
1699
2201
  * The component. A host that passes `wallet` gets the lock screen, the lock button, the authentication type and the Wallet Security section from it (src/wallet/walletProps.tsx); everything else, and every
1700
2202
  * prop the host passes explicitly, is unchanged.
1701
2203
  */
1702
- export default function MeritWallet(props) {
2204
+ function MeritWalletWired(props) {
1703
2205
  const platform = props.platform ?? domPlatform;
1704
2206
  // 2026-10-15 (docs/design/spcoinPackagesDesign.txt 8.5.7 item 4): a host must say where its accounts come from (a wallet, its own account groups, or a chain to self-fetch for). A host that says none gets the component's SAMPLE accounts and tokens, which is
1705
2207
  // never what a deployment wants, so it is reported loudly, once, instead of shipping silently.
@@ -1710,14 +2212,120 @@ export default function MeritWallet(props) {
1710
2212
  }
1711
2213
  }, [props.wallet, props.accountGroups, props.chainId]);
1712
2214
  const derived = useWalletDerivedProps(props.wallet, props.activeAccountAddress, props.walletDerives);
1713
- const wantsAccounts = !!props.approverHost && (props.walletDerives ?? ALL_WALLET_DERIVATIONS).includes('accounts');
1714
- const approvers = useApproverAccounts(wantsAccounts ? props.wallet : undefined, props.approverHost, props.activeAccountAddress, platform);
2215
+ const peerStates = usePeerStates(props.peers);
2216
+ // the host's connection hooks (props.walletLinkHooks): a constant object, so the hooks run in the same order every render
2217
+ const walletLinks = {};
2218
+ /* eslint-disable react-hooks/rules-of-hooks */
2219
+ for (const [id, useLink] of Object.entries(props.walletLinkHooks ?? {}))
2220
+ walletLinks[id] = useLink();
2221
+ /* eslint-enable react-hooks/rules-of-hooks */
2222
+ // 2026-11-07 (owner: Connected must come from the remote, not from the click): while a Connect of a remote wallet waits for the person's answer THERE, its badge stays Disconnected, whatever the link flag says meanwhile
2223
+ const [connectingWallets, setConnectingWallets] = React.useState({});
2224
+ // 2026-11-07 (owner, rules W4): the wallets CHECKED on the wallet list are the groups on the Active Account Selection page
2225
+ const listedWallets = useWalletListed();
2226
+ const listedKey = JSON.stringify(listedWallets);
2227
+ for (const id of Object.keys(walletLinks))
2228
+ if (connectingWallets[id])
2229
+ walletLinks[id] = false;
2230
+ const walletLinksKey = JSON.stringify(walletLinks);
2231
+ // read at click time, so a host that rebuilds these each render does not rebuild the approver list
2232
+ const approversRef = React.useRef(undefined);
2233
+ const addHostRef = React.useRef(undefined);
2234
+ const { openPanel: openWalletListPanel } = usePanelTree();
2235
+ // the running app's own "Add Account(s)": the Add menu (Add Wallet Authenticator or Account)
2236
+ const openAddMenu = useCallback(() => {
2237
+ openWalletListPanel(SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT, 'MeritWallet:addAccounts:addMenu');
2238
+ }, [openWalletListPanel]);
2239
+ // 2026-11-12 (owner): the drop-down's choices open their screen directly (the Add menu is gone); the Add screens are leaves of ADD_WALLET_ACCOUNT, which stays under them
2240
+ const openAddLeaf = useCallback((leaf) => {
2241
+ openWalletListPanel(SP_COIN_DISPLAY.ADD_WALLET_ACCOUNT, 'MeritWallet:addAccounts:leaf');
2242
+ openWalletListPanel(leaf === 'createAccount' ? SP_COIN_DISPLAY.CREATE_ACCOUNT : leaf === 'importAccount' ? SP_COIN_DISPLAY.IMPORT_AN_ACCOUNT : SP_COIN_DISPLAY.IMPORT_A_WALLET, 'MeritWallet:addAccounts:leaf');
2243
+ }, [openWalletListPanel]);
2244
+ const approverHost = useMemo(() => {
2245
+ if (!props.approverHost)
2246
+ return props.approverHost;
2247
+ const withList = {
2248
+ ...props.approverHost,
2249
+ openAddMenu: props.approverHost.openAddMenu ?? openAddMenu,
2250
+ openAddLeaf: props.approverHost.openAddLeaf ?? openAddLeaf,
2251
+ walletLink: (id) => (id in walletLinks ? walletLinks[id] : props.approverHost.walletLink?.(id)),
2252
+ isListed: (id) => {
2253
+ const slug = Object.keys(WALLET_SLUG_ID).find((s) => WALLET_SLUG_ID[s] === id);
2254
+ return slug && id !== (props.nativeWalletId ?? props.approverHost.nativeWalletId) ? !!listedWallets[slug] : undefined;
2255
+ },
2256
+ // 2026-11-07 (rules G4, G6): the OTHER Merit wallet's group badge connects and disconnects it exactly as the wallet list does
2257
+ walletConnect: (id) => {
2258
+ const addHost = addHostRef.current;
2259
+ if (!addHost || id === (props.nativeWalletId ?? props.approverHost.nativeWalletId))
2260
+ return undefined;
2261
+ // 2026-11-07 (owner, rules W5): what the wallet list's Connect used to do now happens here. Every call is made in the click itself (a browser lets a page open another wallet's window, or its device chooser, only in answer to one).
2262
+ // MetaMask: the wallet-level connect (no account becomes active, no screen changes)
2263
+ if (id === WALLET_SLUG_ID.metamask && addHost.connectWallet)
2264
+ return addHost.connectWallet('metamask');
2265
+ // a hardware wallet: its device chooser (Chrome's "wants to connect to a HID device")
2266
+ if (id.startsWith('hardware:')) {
2267
+ if (addHost.connectHardwareDevice)
2268
+ return addHost.connectHardwareDevice(id);
2269
+ return addHost.connectApprover?.('connectHardware');
2270
+ }
2271
+ if (!addHost.connectApprover)
2272
+ return undefined;
2273
+ const entry = id === WALLET_SLUG_ID.meritExtension ? 'connectMeritExtension' : id === WALLET_SLUG_ID.merit ? 'connectMeritWeb' : undefined;
2274
+ if (!entry)
2275
+ return undefined;
2276
+ if (entry === 'connectMeritExtension')
2277
+ addHost.bringWalletForward?.('connectMeritExtension');
2278
+ setConnectingWallets((c) => ({ ...c, [id]: true }));
2279
+ return Promise.resolve(addHost.connectApprover(entry))
2280
+ // the person cancelled (or it failed) there: the link flag a connect sets on the way is undone, the wallet was never connected
2281
+ .catch((error) => { props.peers?.[id]?.detach(); throw error; })
2282
+ .finally(() => setConnectingWallets((c) => ({ ...c, [id]: false })));
2283
+ },
2284
+ walletDisconnect: (id) => {
2285
+ const peer = props.peers?.[id];
2286
+ if (!peer || id === (props.nativeWalletId ?? props.approverHost.nativeWalletId))
2287
+ return undefined;
2288
+ peer.detach();
2289
+ // a peer this wallet has no backend for (the extension's Merit (Web App)) has no permission to revoke: the link flag is the whole disconnect
2290
+ return props.wallet?.backend(id) ? approversRef.current?.controller?.disconnect(id) ?? Promise.resolve() : Promise.resolve();
2291
+ },
2292
+ };
2293
+ return props.peers
2294
+ ? {
2295
+ ...withList,
2296
+ isConnected: (id) => (id in peerStates ? peerStates[id].connected : props.approverHost.isConnected?.(id)),
2297
+ }
2298
+ : withList;
2299
+ },
2300
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- walletLinks is rebuilt every render; walletLinksKey is its value
2301
+ [props.approverHost, props.peers, props.wallet, props.nativeWalletId, peerStates, openAddMenu, openAddLeaf, walletLinksKey, listedKey]);
2302
+ const wantsAccounts = !!approverHost && (props.walletDerives ?? ALL_WALLET_DERIVATIONS).includes('accounts');
2303
+ const approvers = useApproverAccounts(wantsAccounts ? props.wallet : undefined, approverHost, props.activeAccountAddress, platform);
2304
+ approversRef.current = approvers;
2305
+ // 2026-11-08 (owner): a failed connect is a MESSAGE_PANEL error (source, error, wallet), not a banner over the account list
2306
+ const { setErrorMessage: setApproverError } = useExchangeContext();
2307
+ const { openPanel: openApproverPanel } = usePanelTree();
2308
+ useEffect(() => {
2309
+ if (!approvers.notice)
2310
+ return;
2311
+ const wallet = approvers.noticeWalletId ? walletName(approvers.noticeWalletId) : undefined;
2312
+ // 2026-11-12 (owner): "<wallet> is not installed in this browser" carries an Install button: the host's install page for that wallet, else the wallet's own download page (MetaMask)
2313
+ const missingId = approvers.noticeWalletId;
2314
+ const installUrl = missingId && /is not installed/i.test(approvers.notice) ? (approverHost?.installUrl?.(missingId) ?? (/metamask/i.test(missingId) ? 'https://metamask.io/download/' : undefined)) : undefined;
2315
+ setApproverError({ errCode: 0, msg: approvers.notice + (wallet ? String.fromCharCode(10) + 'Wallet: ' + wallet : ''), source: 'Active Account Selection', status: STATUS.MESSAGE_ERROR, ...(installUrl ? { installUrl, installLabel: `Install ${wallet ?? 'wallet'}` } : {}) });
2316
+ openApproverPanel(SP_COIN_DISPLAY.MESSAGE_PANEL, 'MeritWallet:approverNotice');
2317
+ approvers.clearNotice();
2318
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- fires on a new notice only
2319
+ }, [approvers.notice]);
2320
+ addHostRef.current = props.addAccountHost;
2321
+ // 2026-11-06 (owner): the other wallet took accounts out of this wallet's list: a MESSAGE_PANEL popup with the accounts' icons
2322
+ useAccountNoticeMessagePanel(approverHost?.describeAccount);
1715
2323
  const accountDerived = approvers.groups
1716
2324
  ? {
1717
2325
  accountGroups: approvers.groups,
1718
- accountListNotice: approvers.notice || undefined,
1719
2326
  onAccountRowSelect: async (accountId, groupId) => {
1720
2327
  const approverId = (groupId ?? (props.wallet ? props.wallet.backendIdFor(accountId) : undefined));
2328
+ engineTrace('MeritWallet:onAccountRowSelect', { accountId, groupId, approverId, stack: String(new Error().stack ?? '').split(String.fromCharCode(10)).slice(2, 7).join(' | ') });
1721
2329
  if (!approverId || !approvers.controller)
1722
2330
  return false;
1723
2331
  return approvers.controller.connect(approverId, accountId);
@@ -1729,14 +2337,28 @@ export default function MeritWallet(props) {
1729
2337
  const activeWalletName = activeWalletId && props.wallet ? props.wallet.backend(activeWalletId)?.name : undefined;
1730
2338
  const nativeWalletId = props.nativeWalletId ?? props.approverHost?.nativeWalletId;
1731
2339
  // the wallet list's Connected / Disconnected buttons, from the approver list (a wallet is connected when the host says so or the connected-only list holds it)
1732
- const connectionOfWallet = { merit: 'serverKeystore', meritExtension: 'injected:org.sponsorcoin.merit', metamask: 'injected:io.metamask', ledger: 'hardware:ledger', trezor: 'hardware:trezor' };
2340
+ const connectionOfWallet = WALLET_SLUG_ID;
1733
2341
  const derivedConnection = approvers.groups
1734
2342
  ? {
1735
- walletConnection: props.walletConnection ?? Object.fromEntries(Object.entries(connectionOfWallet).map(([slug, id]) => [slug, props.approverHost?.isConnected?.(id) ?? approvers.groups.some((g) => g.id === id)])),
1736
- onDisconnectWallet: props.onDisconnectWallet ?? ((slug) => { const id = connectionOfWallet[slug]; if (id)
1737
- void approvers.controller?.disconnect(id); }),
2343
+ walletConnection: props.walletConnection ?? Object.fromEntries(Object.entries(connectionOfWallet).map(([slug, id]) => [slug, approverHost?.isConnected?.(id) ?? approvers.groups.some((g) => g.id === id)])),
2344
+ onDisconnectWallet: props.onDisconnectWallet ?? (async (slug) => { const id = connectionOfWallet[slug]; if (id) {
2345
+ const peer = props.peers?.[id];
2346
+ peer?.detach(); /* a peer this wallet has no backend for (the extension's Merit (Web App)) has no permission to revoke: the link flag is the whole disconnect */
2347
+ if (!peer || props.wallet?.backend(id))
2348
+ await approvers.controller?.disconnect(id);
2349
+ } }),
1738
2350
  }
1739
2351
  : {};
1740
2352
  const merged = props.wallet ? { ...mergeWalletProps(props, { ...derived, activeWalletName }, props.wallet, props.activeAccountAddress), activeWalletId: props.activeWalletId ?? activeWalletId, nativeWalletId, ...accountDerived, ...derivedConnection } : { ...props, nativeWalletId };
1741
2353
  return React.createElement(MeritPlatformContext.Provider, { value: platform }, React.createElement(MeritWalletInner, merged));
1742
2354
  }
2355
+ /**
2356
+ * The component. First-run setup replaces the wallet (a different component type, so the wallet's own state starts fresh when setup ends, the same as when the host used to swap it out);
2357
+ * otherwise the wired wallet. The setup screen runs inside the same platform context (clipboard) as the wallet.
2358
+ */
2359
+ export default function MeritWallet(props) {
2360
+ if (props.onboarding) {
2361
+ return React.createElement(MeritPlatformContext.Provider, { value: props.platform ?? domPlatform }, React.createElement(WalletOnboardingPanel, props.onboarding));
2362
+ }
2363
+ return React.createElement(MeritWalletWired, props);
2364
+ }