@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.
- package/README.md +22 -0
- package/dist/AgentSelectDropDown.js +4 -2
- package/dist/AgentSelectDropDownWithAvatar.js +2 -1
- package/dist/ChangePasswordPanel.js +6 -6
- package/dist/ConnectedMessagePanel.js +17 -15
- package/dist/ConnectedNetworkPanel.js +7 -12
- package/dist/DeleteWalletDialog.js +59 -12
- package/dist/DialogPortal.d.ts +3 -0
- package/dist/DialogPortal.js +19 -0
- package/dist/MeritWallet.d.ts +47 -47
- package/dist/MeritWallet.js +1384 -762
- package/dist/PasswordPromptDialog.js +8 -6
- package/dist/RecoveryPhraseScreen.js +3 -7
- package/dist/SecuritySetupScreen.d.ts +44 -0
- package/dist/SecuritySetupScreen.js +119 -0
- package/dist/SecurityWarningScreen.d.ts +5 -0
- package/dist/SecurityWarningScreen.js +18 -0
- package/dist/TestAccountsSection.js +2 -1
- package/dist/TokenPanel.js +29 -9
- package/dist/TokenSlotPanels.js +7 -8
- package/dist/UnlockSecondStep.d.ts +7 -0
- package/dist/UnlockSecondStep.js +50 -0
- package/dist/VaultAccountsPanel.js +22 -22
- package/dist/WalletOnboardingPanel.js +11 -12
- package/dist/WalletSecuritySection.js +4 -15
- package/dist/account/AccountDetailScreen.d.ts +7 -2
- package/dist/account/AccountDetailScreen.js +52 -28
- package/dist/account/AccountPrivateKeyRow.d.ts +6 -0
- package/dist/account/AccountPrivateKeyRow.js +6 -2
- package/dist/account/AddAccountFlow.d.ts +18 -18
- package/dist/account/AddAccountFlow.js +159 -121
- package/dist/account/DetailImageBlock.d.ts +44 -0
- package/dist/account/DetailImageBlock.js +46 -0
- package/dist/account/DetailScreenLayout.d.ts +38 -0
- package/dist/account/DetailScreenLayout.js +30 -0
- package/dist/account/MetaDetailScreen.d.ts +30 -0
- package/dist/account/MetaDetailScreen.js +33 -0
- package/dist/account/RemoveAccountPanel.d.ts +14 -0
- package/dist/account/RemoveAccountPanel.js +97 -0
- package/dist/account/accountDetailHostFromProfile.d.ts +4 -2
- package/dist/account/accountDetailHostFromProfile.js +3 -2
- package/dist/account/accountSource.d.ts +36 -0
- package/dist/account/accountSource.js +82 -0
- package/dist/account/form/AccountAvatarPanel.js +12 -235
- package/dist/account/form/AccountFormPanel.d.ts +6 -2
- package/dist/account/form/AccountFormPanel.js +82 -114
- package/dist/account/form/AvatarControls.d.ts +25 -0
- package/dist/account/form/AvatarControls.js +27 -0
- package/dist/account/form/AvatarPicture.d.ts +11 -0
- package/dist/account/form/AvatarPicture.js +11 -0
- package/dist/account/form/CopyIcon.d.ts +1 -0
- package/dist/account/form/CopyIcon.js +7 -0
- package/dist/account/form/DisconnectedControl.js +2 -1
- package/dist/account/form/formTypes.d.ts +0 -1
- package/dist/account/form/useAccountForm.d.ts +8 -2
- package/dist/account/form/useAccountForm.js +79 -44
- package/dist/account/form/useAccountFormDerivedState.js +1 -1
- package/dist/account/form/useAccountLiveRefresh.d.ts +15 -0
- package/dist/account/form/useAccountLiveRefresh.js +46 -0
- package/dist/account/form/useAvatarPreviewSize.d.ts +39 -0
- package/dist/account/form/useAvatarPreviewSize.js +245 -0
- package/dist/account/responseFailure.d.ts +1 -0
- package/dist/account/responseFailure.js +7 -0
- package/dist/account/signIn.js +3 -6
- package/dist/approval/approvalLifecycle.d.ts +15 -0
- package/dist/approval/approvalLifecycle.js +51 -0
- package/dist/approval/meritApproval.js +48 -3
- package/dist/approval/pendingApprovalMessageStore.d.ts +7 -0
- package/dist/approval/pendingApprovalMessageStore.js +18 -0
- package/dist/approval/useMeritApprovalMessagePanel.d.ts +7 -0
- package/dist/approval/useMeritApprovalMessagePanel.js +132 -0
- package/dist/auth/accountNoticeStore.d.ts +22 -0
- package/dist/auth/accountNoticeStore.js +31 -0
- package/dist/auth/approvalWaiting.d.ts +4 -2
- package/dist/auth/approvalWaiting.js +16 -2
- package/dist/auth/authenticator.d.ts +29 -4
- package/dist/auth/authenticator.js +3 -2
- package/dist/auth/connect.js +1 -1
- package/dist/auth/injectedAuthenticator.d.ts +4 -1
- package/dist/auth/injectedAuthenticator.js +28 -1
- package/dist/auth/keptAccounts.d.ts +14 -0
- package/dist/auth/keptAccounts.js +126 -0
- package/dist/auth/meritConnection.d.ts +31 -0
- package/dist/auth/meritConnection.js +65 -0
- package/dist/auth/registerApprovingWallet.d.ts +1 -0
- package/dist/auth/registerApprovingWallet.js +15 -0
- package/dist/auth/useAccountNoticeMessagePanel.d.ts +5 -0
- package/dist/auth/useAccountNoticeMessagePanel.js +54 -0
- package/dist/auth/useApprovalMessagePanel.js +16 -6
- package/dist/auth/walletAuthenticators.d.ts +35 -0
- package/dist/auth/walletAuthenticators.js +83 -0
- package/dist/auth/walletAuthenticatorsReact.d.ts +9 -0
- package/dist/auth/walletAuthenticatorsReact.js +14 -0
- package/dist/balance/createBalanceReader.d.ts +23 -0
- package/dist/balance/createBalanceReader.js +24 -0
- package/dist/chain/chainReconcile.d.ts +68 -0
- package/dist/chain/chainReconcile.js +139 -0
- package/dist/config/WalletSessionDialogs.js +3 -1
- package/dist/debug/DebugBug.d.ts +8 -0
- package/dist/debug/DebugBug.js +74 -0
- package/dist/debug/debugStore.d.ts +39 -0
- package/dist/debug/debugStore.js +148 -0
- package/dist/hostContract.d.ts +6 -6
- package/dist/index.d.ts +52 -18
- package/dist/index.js +46 -13
- package/dist/injectableComponentTypes.d.ts +97 -0
- package/dist/injectableComponentTypes.js +12 -0
- package/dist/keystore/index.d.ts +3 -0
- package/dist/keystore/index.js +10 -0
- package/dist/keystore/publicAccountStore.d.ts +14 -0
- package/dist/keystore/publicAccountStore.js +29 -0
- package/dist/keystore/secretCache.d.ts +20 -0
- package/dist/keystore/secretCache.js +48 -0
- package/dist/panels/AcknowledgeDialog.d.ts +6 -0
- package/dist/panels/AcknowledgeDialog.js +14 -0
- package/dist/panels/ActiveAccountSelectDropDown.d.ts +22 -0
- package/dist/panels/ActiveAccountSelectDropDown.js +57 -0
- package/dist/panels/AddressPanel.js +2 -1
- package/dist/panels/AgentSelectDropDown.js +3 -36
- package/dist/panels/AnonymousAvatar.js +2 -1
- package/dist/panels/DetailRow.d.ts +11 -0
- package/dist/panels/DetailRow.js +13 -0
- package/dist/panels/MenuTabHeaderBar.js +6 -64
- package/dist/panels/MeritInfoPanel.js +2 -2
- package/dist/panels/MeritInfoPanelReal.js +5 -3
- package/dist/panels/MessageDetailsSection.js +2 -1
- package/dist/panels/MessageLabelValueRow.js +2 -1
- package/dist/panels/MessagePanel.js +2 -18
- package/dist/panels/MessagePanelReal.d.ts +51 -1
- package/dist/panels/MessagePanelReal.js +123 -20
- package/dist/panels/NetworkDetailPanel.js +3 -46
- package/dist/panels/NetworkSelectDropDown.d.ts +5 -1
- package/dist/panels/NetworkSelectDropDown.js +8 -5
- package/dist/panels/PanelTitle.js +3 -67
- package/dist/panels/ReadOnlyMetaDataTable.js +12 -11
- package/dist/panels/SendTabPanel.d.ts +10 -1
- package/dist/panels/SendTabPanel.js +16 -4
- package/dist/panels/TokenAddressComponent.js +4 -6
- package/dist/panels/TokenDetailPanel.js +11 -48
- package/dist/panels/TokenSelectDropDown.d.ts +7 -1
- package/dist/panels/TokenSelectDropDown.js +7 -8
- package/dist/panels/TradingStationPanel.js +4 -15
- package/dist/panels/WalletAccountHeader.d.ts +5 -5
- package/dist/panels/WalletAccountHeader.js +11 -40
- package/dist/panels/WalletAccountSelectDropDown.d.ts +15 -0
- package/dist/panels/WalletAccountSelectDropDown.js +42 -0
- package/dist/panels/WalletRadioPanels.d.ts +9 -12
- package/dist/panels/WalletRadioPanels.js +97 -41
- package/dist/panels/clipboardClear.d.ts +7 -0
- package/dist/panels/clipboardClear.js +31 -0
- package/dist/panels/index.d.ts +3 -11
- package/dist/panels/index.js +1 -6
- package/dist/panels/meritWalletDataFetch.d.ts +20 -10
- package/dist/panels/meritWalletDataFetch.js +53 -13
- package/dist/panels/useChainInfo.d.ts +5 -0
- package/dist/panels/useChainInfo.js +28 -0
- package/dist/peer/peerConnection.d.ts +43 -0
- package/dist/peer/peerConnection.js +126 -0
- package/dist/platform/domPlatform.js +111 -1
- package/dist/platform/platform.d.ts +19 -0
- package/dist/provider/useMeritLink.d.ts +8 -0
- package/dist/provider/useMeritLink.js +8 -0
- package/dist/provider/useMetaMask.d.ts +11 -0
- package/dist/provider/useMetaMask.js +67 -0
- package/dist/radioPanelGroups.d.ts +8 -10
- package/dist/radioPanelGroups.js +47 -13
- package/dist/receipt/transactionReceipts.d.ts +27 -0
- package/dist/receipt/transactionReceipts.js +25 -0
- package/dist/refresh/refreshReact.d.ts +30 -0
- package/dist/refresh/refreshReact.js +101 -0
- package/dist/refresh/refreshTree.d.ts +42 -0
- package/dist/refresh/refreshTree.js +81 -0
- package/dist/rewards/ConnectedRewardsPanel.d.ts +6 -1
- package/dist/rewards/ConnectedRewardsPanel.js +58 -10
- package/dist/rewards/accountRoleFlags.d.ts +1 -0
- package/dist/rewards/accountRoleFlags.js +25 -0
- package/dist/rewards/chainClock.d.ts +17 -0
- package/dist/rewards/chainClock.js +63 -0
- package/dist/rewards/estimateRewards.d.ts +11 -3
- package/dist/rewards/estimateRewards.js +76 -2
- package/dist/security/mfaFactorsStore.d.ts +16 -0
- package/dist/security/mfaFactorsStore.js +55 -0
- package/dist/security/passkey.d.ts +20 -0
- package/dist/security/passkey.js +60 -0
- package/dist/security/signInOptions.d.ts +20 -0
- package/dist/security/signInOptions.js +12 -0
- package/dist/security/totp.d.ts +13 -0
- package/dist/security/totp.js +77 -0
- package/dist/send/SendComponent.js +2 -2
- package/dist/send/sendTransfer.d.ts +2 -2
- package/dist/send/sendTransfer.js +3 -17
- package/dist/session/activeAccountCore.d.ts +81 -0
- package/dist/session/activeAccountCore.js +57 -0
- package/dist/session/indexedDbKey.d.ts +12 -0
- package/dist/session/indexedDbKey.js +39 -0
- package/dist/session/persistedPassword.d.ts +23 -0
- package/dist/session/persistedPassword.js +65 -0
- package/dist/session/validateLogin.d.ts +11 -0
- package/dist/session/validateLogin.js +11 -0
- package/dist/session/walletSession.d.ts +9 -0
- package/dist/session/walletSession.js +10 -4
- package/dist/session/walletSessionReact.d.ts +1 -0
- package/dist/session/walletSessionReact.js +2 -1
- package/dist/sponsor/ConnectedSponsorPanel.d.ts +7 -0
- package/dist/sponsor/ConnectedSponsorPanel.js +32 -0
- package/dist/sponsor/ConnectedSponsorStakingList.d.ts +15 -0
- package/dist/sponsor/ConnectedSponsorStakingList.js +205 -35
- package/dist/sponsor/sponsorReads.d.ts +13 -0
- package/dist/sponsor/sponsorReads.js +59 -0
- package/dist/sponsor/stakingListMode.d.ts +9 -0
- package/dist/sponsor/stakingListMode.js +20 -0
- package/dist/swap/parseDecimalAmount.d.ts +5 -0
- package/dist/swap/parseDecimalAmount.js +26 -0
- package/dist/sync/pushContextPatch.d.ts +24 -0
- package/dist/sync/pushContextPatch.js +19 -0
- package/dist/sync/syncToken.d.ts +52 -0
- package/dist/sync/syncToken.js +128 -0
- package/dist/trade/tradeHost.d.ts +37 -0
- package/dist/trade/tradeHost.js +29 -0
- package/dist/vault/providerController.d.ts +21 -0
- package/dist/vault/providerController.js +93 -9
- package/dist/vault/vaultCapability.d.ts +4 -0
- package/dist/vault/vaultCapability.js +5 -1
- package/dist/vault/walletAccounts.d.ts +9 -1
- package/dist/vault/walletAccounts.js +27 -3
- package/dist/vault/workerVault.js +7 -1
- package/dist/wallet/AccountSigningSection.js +4 -5
- package/dist/wallet/WalletBackupSection.js +2 -4
- package/dist/wallet/accountHolders.d.ts +9 -0
- package/dist/wallet/accountHolders.js +22 -0
- package/dist/wallet/accountSigning.js +6 -1
- package/dist/wallet/approverAccounts.d.ts +42 -2
- package/dist/wallet/approverAccounts.js +110 -19
- package/dist/wallet/approverAccountsReact.d.ts +3 -0
- package/dist/wallet/approverAccountsReact.js +118 -18
- package/dist/wallet/sameAddress.d.ts +1 -0
- package/dist/wallet/sameAddress.js +4 -0
- package/dist/wallet/wallet.d.ts +10 -2
- package/dist/wallet/wallet.js +61 -28
- package/dist/wallet/walletSlugs.d.ts +2 -0
- package/dist/wallet/walletSlugs.js +7 -0
- package/dist/walletConfig.d.ts +13 -0
- package/dist/walletConfig.js +53 -4
- package/dist/walletConfigPanelProps.d.ts +7 -1
- package/dist/walletConfigPanelProps.js +17 -5
- package/dist/walletData.d.ts +2 -2
- package/package.json +4 -7
- package/dist/ConnectedPasswordPanel.d.ts +0 -13
- package/dist/ConnectedPasswordPanel.js +0 -30
- package/dist/WalletOverlayHost.d.ts +0 -35
- package/dist/WalletOverlayHost.js +0 -46
- package/dist/account/accountProfileClient.d.ts +0 -18
- package/dist/account/accountProfileClient.js +0 -50
- package/dist/auth/ApprovalWaitingNotice.d.ts +0 -4
- package/dist/auth/ApprovalWaitingNotice.js +0 -20
- package/dist/panels/DetailPanelEmptyState.d.ts +0 -10
- package/dist/panels/DetailPanelEmptyState.js +0 -19
- package/dist/panels/GenericListPanel.d.ts +0 -15
- package/dist/panels/GenericListPanel.js +0 -33
- package/dist/panels/RewardsPendingByAccountTypePanel.d.ts +0 -9
- package/dist/panels/RewardsPendingByAccountTypePanel.js +0 -39
- package/dist/panels/SendAddressHeaderBar.d.ts +0 -14
- package/dist/panels/SendAddressHeaderBar.js +0 -80
- package/dist/panels/SendTitle.d.ts +0 -1
- package/dist/panels/SendTitle.js +0 -13
- package/dist/panels/SendToAddressComponent.d.ts +0 -5
- package/dist/panels/SendToAddressComponent.js +0 -13
package/dist/MeritWallet.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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 {
|
|
42
|
-
import {
|
|
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 {
|
|
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 {
|
|
69
|
-
//
|
|
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.
|
|
166
|
-
buyToken: SP_COIN_DISPLAY.
|
|
167
|
-
sendToken: SP_COIN_DISPLAY.
|
|
168
|
-
sendRecipient: SP_COIN_DISPLAY.
|
|
169
|
-
account: SP_COIN_DISPLAY.
|
|
170
|
-
network: SP_COIN_DISPLAY.
|
|
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({
|
|
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/
|
|
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
|
|
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.
|
|
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.
|
|
319
|
+
closePanel(SP_COIN_DISPLAY.ZERO_X_CONFIG_SLIPPAGE_PANEL, 'MeritWallet:slippageCog:close');
|
|
304
320
|
}
|
|
305
321
|
else {
|
|
306
|
-
openPanel(SP_COIN_DISPLAY.
|
|
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
|
-
|
|
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-
|
|
363
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
455
|
-
.catch((error) => console.error('MeritWallet: failed to self-fetch
|
|
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
|
-
//
|
|
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
|
|
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) => ({
|
|
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: `
|
|
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
|
-
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
const
|
|
553
|
-
|
|
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
|
-
|
|
645
|
+
openActiveListPanel(paramsForMode(mode), 'MeritWallet:setActiveList', listPanelIdFor(mode));
|
|
559
646
|
}
|
|
560
647
|
else {
|
|
561
648
|
activeListPanelStore.setWalletListSelectionSlot(null);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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 =
|
|
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 =
|
|
639
|
-
const sellBalance =
|
|
640
|
-
const buyBalance =
|
|
641
|
-
|
|
642
|
-
const
|
|
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
|
|
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
|
-
|
|
710
|
-
const
|
|
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
|
-
|
|
713
|
-
|
|
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 [
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 (
|
|
751
|
-
if
|
|
752
|
-
|
|
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
|
-
//
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
//
|
|
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
|
-
//
|
|
872
|
-
|
|
873
|
-
//
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
? (
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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-
|
|
1199
|
-
//
|
|
1200
|
-
//
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
//
|
|
1204
|
-
//
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1320
|
-
|
|
1715
|
+
openAccountDetail(account.address, group.id);
|
|
1716
|
+
onAccountIconClick?.(account.address);
|
|
1321
1717
|
}
|
|
1322
1718
|
: undefined,
|
|
1323
1719
|
})),
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
?
|
|
1329
|
-
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
-
:
|
|
1509
|
-
|
|
1510
|
-
|
|
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
|
-
|
|
1515
|
-
|
|
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(
|
|
1539
|
-
: passwordPanelShowing
|
|
1540
|
-
? React.createElement(
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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:
|
|
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
|
|
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
|
|
1562
|
-
|
|
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 =
|
|
1579
|
-
?
|
|
1580
|
-
:
|
|
1581
|
-
?
|
|
1582
|
-
:
|
|
1583
|
-
?
|
|
1584
|
-
:
|
|
1585
|
-
? '
|
|
1586
|
-
:
|
|
1587
|
-
?
|
|
1588
|
-
:
|
|
1589
|
-
|
|
1590
|
-
:
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
? '
|
|
1596
|
-
:
|
|
1597
|
-
? '
|
|
1598
|
-
:
|
|
1599
|
-
? '
|
|
1600
|
-
:
|
|
1601
|
-
? '
|
|
1602
|
-
:
|
|
1603
|
-
? '
|
|
1604
|
-
:
|
|
1605
|
-
? '
|
|
1606
|
-
:
|
|
1607
|
-
? '
|
|
1608
|
-
:
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
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
|
-
|
|
1676
|
-
|
|
2126
|
+
openNetworkDetail(activeNetworkRow.id);
|
|
2127
|
+
onNetworkIconClick?.(activeNetworkRow.id);
|
|
1677
2128
|
}
|
|
1678
|
-
:
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
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
|
-
|
|
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
|
|
1714
|
-
|
|
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 =
|
|
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,
|
|
1736
|
-
onDisconnectWallet: props.onDisconnectWallet ?? ((slug) => { const id = connectionOfWallet[slug]; if (id)
|
|
1737
|
-
|
|
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
|
+
}
|