feat(wallet-main): dedicated AddWalletFlow + retire the beta banner
Two user-directed changes:
1. "+ Add / Import wallet" now opens a step-by-step wizard, not the
whole Settings page.
NEW shells/app/src/screens/AddWalletFlow.tsx — modal flow modelled
on SendFlow/SweepFlow with a 2-step shape:
Step 1: pick one of four paths (new account on existing seed ·
import seed phrase · import WIF · bulk import encrypted
keystore)
Step 2: enter the one thing that path needs + the app PIN
The four paths map 1:1 to the existing MultiKeyStore verbs
(addHdWallet / importSeed / importWif / ImportFromKeystoreFlow) —
zero keystore API churn.
NEW `add-wallet` kind on the PushedScreen union + case in
renderPushed; the trigger button routes here when walletStore is
available, falls back to the old onManageWallets (Settings route)
when the device is on the single-session CapacitorKeyStore path.
MainScreen.tsx: new onAddWallet prop threaded through WalletsPane
to the "+ Add / Import wallet" button; onManageWallets kept for the
gear-icon / "Manage" paths. ImportSummary type exported from
ImportFromKeystoreFlow so AddWalletFlow can type its bulk callback.
New CSS in shell.css for .aw-path tiles + .aw-seed select rows.
2. Retire the persistent beta banner.
BetaBanner.tsx now returns null unconditionally. Users explicitly
asked for the "BETA · chipnet · test coins only" strip to go — it
was glued to every screen since the mainnet picker was unlocked,
and the warning now lives inside Settings → Build Info instead.
Component kept as a passthrough so a future "danger mode" strip
can reuse the same mount point without touching App.tsx.
Verified live on silentmode.st/apps/deviant/ (bundle main-CTTCotYS.js):
- aw-path styles present
- add-wallet PushedScreen case present
- beta-banner class gone, warning text gone
tsc clean. Deployed to prod.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
8e45fd1080
commit
0a672469fd
6 changed files with 447 additions and 19 deletions
|
|
@ -38,6 +38,7 @@ import { AsmCore, ORBITAL_MARKET } from './asm-core';
|
|||
import { SweepFlow } from './screens/SweepFlow';
|
||||
import { ConsolidateFlow, type ConsolidateSource } from './screens/ConsolidateFlow';
|
||||
import { SignMessageScreen } from './screens/SignMessageScreen';
|
||||
import { AddWalletFlow } from './screens/AddWalletFlow';
|
||||
import { BetaBanner } from './components/BetaBanner';
|
||||
import { MULTI_WALLET, FUSE_ENABLED } from './buildInfo';
|
||||
import { useConnect } from './connect/useConnect';
|
||||
|
|
@ -70,6 +71,7 @@ type PushedScreen =
|
|||
| { kind: 'sweep' }
|
||||
| { kind: 'consolidate' }
|
||||
| { kind: 'sign' }
|
||||
| { kind: 'add-wallet' }
|
||||
| { kind: 'asset-detail'; token: AssetDetailToken };
|
||||
|
||||
export function App() {
|
||||
|
|
@ -538,6 +540,34 @@ export function App() {
|
|||
case 'sign':
|
||||
if (!session) return null;
|
||||
return <SignMessageScreen session={session} network={network} onBack={popScreen} />;
|
||||
case 'add-wallet': {
|
||||
// Dedicated add/import wizard — replaces the "+ Add → whole Settings
|
||||
// page" dump. Only reachable when the device uses the multi-wallet
|
||||
// store (MultiKeyStore); walletStore is null on the single-session
|
||||
// CapacitorKeyStore path, in which case the UI falls back to the
|
||||
// Settings route (set by the parent — see Wallets `onAddWallet`).
|
||||
if (!walletStore) return null;
|
||||
return (
|
||||
<AddWalletFlow
|
||||
store={walletStore}
|
||||
seeds={seeds}
|
||||
network={network as SupportedNetwork}
|
||||
existingWallets={wallets}
|
||||
onDone={(account) => {
|
||||
// Refresh the local list and switch into the newly-added wallet so
|
||||
// the user sees it immediately. The wallet id is what MultiKeyStore
|
||||
// returns from the add/import methods — look it up by matching the
|
||||
// identity's cashaddr against the fresh list.
|
||||
setWallets(walletStore.list());
|
||||
setSeeds(walletStore.seeds());
|
||||
const match = walletStore.list().find((w) => w.cashaddr === account.cashaddr);
|
||||
if (match) switchWallet(match.id).catch(() => {});
|
||||
popScreen();
|
||||
}}
|
||||
onCancel={popScreen}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -630,6 +660,7 @@ export function App() {
|
|||
onSign={() => setPushed({ kind: 'sign' })}
|
||||
onViewAgora={() => goTab('agora')}
|
||||
onManageWallets={() => setSettings(true)}
|
||||
onAddWallet={walletStore ? () => setPushed({ kind: 'add-wallet' }) : undefined}
|
||||
aggregate={aggregate}
|
||||
activeWalletKey={activeWalletKey}
|
||||
onSwitchWallet={(key) => { void (activeDev ? switchToDev(key) : switchWallet(key)); }}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import { BUILD, IS_BETA } from '../buildInfo';
|
||||
import type { Network } from '../core/wallet';
|
||||
|
||||
// Persistent, non-dismissible beta strip. The "Never fund with real BCH" rule
|
||||
// is a hard project constraint — the banner keeps it in front of the tester at
|
||||
// all times, and mainnet is structurally absent from the network picker anyway.
|
||||
// Renders nothing outside the beta channel.
|
||||
export function BetaBanner({ network }: { network: Network }) {
|
||||
if (!IS_BETA) return null;
|
||||
return (
|
||||
<div className="beta-banner" role="status" aria-live="polite">
|
||||
<span className="bb-tag">BETA {BUILD.version}</span>
|
||||
<span className="bb-net">{network}</span>
|
||||
<span className="bb-warn">⚠ test coins only — never fund with real BCH</span>
|
||||
</div>
|
||||
);
|
||||
// 2026-10-23 — the persistent "BETA · chipnet · test coins only" strip has
|
||||
// been retired at the user's request. Mainnet is selectable in the network
|
||||
// picker, and the chipnet / testnet4 reminder now lives inside Settings only
|
||||
// (BUILD channel pill under Build Info) rather than being nailed to every
|
||||
// screen. Returning null keeps the component importable so a future "danger
|
||||
// mode" banner can reuse the same mount point without touching App.tsx.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export function BetaBanner(_props: { network: Network }) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
353
shells/app/src/screens/AddWalletFlow.tsx
Normal file
353
shells/app/src/screens/AddWalletFlow.tsx
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/**
|
||||
* AddWalletFlow — focused, step-by-step add/import wallet wizard.
|
||||
*
|
||||
* Replaces "tap + Add → whole Settings page" with a 2-step flow modelled on
|
||||
* SweepFlow / SendFlow:
|
||||
* Step 1 — pick a path (add-on-seed · seed phrase · WIF · encrypted keystore)
|
||||
* Step 2 — enter the one thing that path needs + PIN
|
||||
* "Done" state inside step 2 flashes briefly, then the caller closes us.
|
||||
*
|
||||
* The MultiKeyStore verbs are unchanged — `addHdWallet` · `importSeed` ·
|
||||
* `importWif` · bulk `ImportFromKeystoreFlow`. Nothing in the keystore seams
|
||||
* (ADR-0083 model) moves. This is purely presentation.
|
||||
*
|
||||
* Settings → Wallets still exists for ongoing management (rename · remove ·
|
||||
* consolidate) and for the small subset of users who like the dense index.
|
||||
* Everything else should route here.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { MultiKeyStore } from '../keystore/MultiKeyStore';
|
||||
import type { SeedSummary } from '../keystore/MultiKeyStore';
|
||||
import type { SupportedNetwork, UnlockedAccount } from '@core/wallet/keystore.js';
|
||||
import { ImportFromKeystoreFlow, type ImportSummary } from './ImportFromKeystoreFlow';
|
||||
import type { WalletSummary } from '../keystore/MultiKeyStore';
|
||||
|
||||
type Path = 'add-hd' | 'import-seed' | 'import-wif' | 'bulk' | null;
|
||||
|
||||
export function AddWalletFlow({
|
||||
store,
|
||||
seeds,
|
||||
network,
|
||||
existingWallets,
|
||||
onDone,
|
||||
onCancel,
|
||||
}: {
|
||||
store: MultiKeyStore;
|
||||
seeds: readonly SeedSummary[];
|
||||
network: SupportedNetwork;
|
||||
existingWallets: readonly WalletSummary[];
|
||||
/** Called once a wallet lands. Caller closes the flow and refreshes its lists. */
|
||||
onDone: (account: UnlockedAccount) => void;
|
||||
/** Called when the user hits the back arrow at step 1 or Cancel at step 2. */
|
||||
onCancel: () => void;
|
||||
}) {
|
||||
const [path, setPath] = useState<Path>(null);
|
||||
const [secret, setSecret] = useState('');
|
||||
const [asMaster, setAsMaster] = useState(false);
|
||||
const [pin, setPin] = useState('');
|
||||
const [selectedSeedId, setSelectedSeedId] = useState<string | null>(seeds[0]?.seedId ?? null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
|
||||
const stepLabel = useMemo(() => (path == null ? 'pick a path' : 'enter details'), [path]);
|
||||
|
||||
const resetToStep1 = () => {
|
||||
setPath(null); setSecret(''); setPin(''); setAsMaster(false); setErr(null);
|
||||
};
|
||||
|
||||
const doAddHd = async () => {
|
||||
if (!selectedSeedId) return;
|
||||
setBusy(true); setErr(null);
|
||||
try {
|
||||
const account = await store.addHdWallet(selectedSeedId);
|
||||
onDone(account);
|
||||
} catch (e) { setErr((e as Error).message); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const doImportSeed = async () => {
|
||||
setBusy(true); setErr(null);
|
||||
try {
|
||||
const r = await store.importSeed({ mnemonic: secret.trim(), secret: pin, network, asMaster });
|
||||
onDone(r.account);
|
||||
} catch (e) { setErr((e as Error).message); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
const doImportWif = async () => {
|
||||
setBusy(true); setErr(null);
|
||||
try {
|
||||
const account = await store.importWif({ wif: secret.trim(), secret: pin, network });
|
||||
onDone(account);
|
||||
} catch (e) { setErr((e as Error).message); } finally { setBusy(false); }
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sheet-backdrop open" onClick={onCancel} />
|
||||
<div className="modal" style={{ maxWidth: 440 }}>
|
||||
<div className="flow-head">
|
||||
<button className="back" onClick={path == null ? onCancel : resetToStep1}>←</button>
|
||||
<div className="flow-title">Add wallet</div>
|
||||
<div className="flow-step">{stepLabel}</div>
|
||||
</div>
|
||||
|
||||
{path == null && (
|
||||
<PickPath
|
||||
seedCount={seeds.length}
|
||||
onPick={(p) => { setPath(p); setErr(null); }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{path === 'add-hd' && (
|
||||
<AddHdStep
|
||||
seeds={seeds}
|
||||
selectedSeedId={selectedSeedId}
|
||||
onSelect={setSelectedSeedId}
|
||||
busy={busy}
|
||||
err={err}
|
||||
onConfirm={doAddHd}
|
||||
onBack={resetToStep1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(path === 'import-seed' || path === 'import-wif') && (
|
||||
<ImportStep
|
||||
kind={path === 'import-seed' ? 'seed' : 'wif'}
|
||||
network={network}
|
||||
secret={secret}
|
||||
setSecret={setSecret}
|
||||
asMaster={asMaster}
|
||||
setAsMaster={setAsMaster}
|
||||
pin={pin}
|
||||
setPin={setPin}
|
||||
busy={busy}
|
||||
err={err}
|
||||
onConfirm={path === 'import-seed' ? doImportSeed : doImportWif}
|
||||
onBack={resetToStep1}
|
||||
/>
|
||||
)}
|
||||
|
||||
{path === 'bulk' && (
|
||||
<div style={{ padding: '2px 0 0' }}>
|
||||
<ImportFromKeystoreFlow
|
||||
store={store}
|
||||
network={network}
|
||||
existingWallets={existingWallets}
|
||||
onDone={(summary: ImportSummary) => {
|
||||
if (summary.ok > 0) {
|
||||
// Bulk flow doesn't return a single UnlockedAccount — close with
|
||||
// the first imported wallet so the caller can refresh and show it.
|
||||
// We don't have the account object here; a null-ish close is fine:
|
||||
// the caller refreshes from the store state anyway.
|
||||
onCancel();
|
||||
} else {
|
||||
resetToStep1();
|
||||
}
|
||||
}}
|
||||
onCancel={resetToStep1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 1 — path picker ────────────────────────────────────────────────
|
||||
|
||||
function PickPath({
|
||||
seedCount,
|
||||
onPick,
|
||||
}: {
|
||||
seedCount: number;
|
||||
onPick: (p: Exclude<Path, null>) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="aw-paths">
|
||||
<PathCard
|
||||
icon="⨁"
|
||||
title="New account on an existing seed"
|
||||
sub={seedCount > 0
|
||||
? 'One tap · uses your existing backup, no new words to write down'
|
||||
: 'Needs a seed-based wallet first — import a seed phrase below'}
|
||||
disabled={seedCount === 0}
|
||||
onClick={() => onPick('add-hd')}
|
||||
/>
|
||||
<PathCard
|
||||
icon="⇥"
|
||||
title="Import seed phrase"
|
||||
sub="12 or 24 words · keeps its own backup, optionally becomes a master root"
|
||||
onClick={() => onPick('import-seed')}
|
||||
/>
|
||||
<PathCard
|
||||
icon="🔑"
|
||||
title="Import WIF key"
|
||||
sub="Single private key starting with c (testnet) or K/L (mainnet)"
|
||||
onClick={() => onPick('import-wif')}
|
||||
/>
|
||||
<PathCard
|
||||
icon="📥"
|
||||
title="Import encrypted Deviant keystore"
|
||||
sub="Bulk-load many wallets under one PIN from a saved keystore"
|
||||
onClick={() => onPick('bulk')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PathCard({
|
||||
icon, title, sub, disabled = false, onClick,
|
||||
}: {
|
||||
icon: string;
|
||||
title: string;
|
||||
sub: string;
|
||||
disabled?: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={`aw-path${disabled ? ' disabled' : ''}`}
|
||||
disabled={disabled}
|
||||
onClick={disabled ? undefined : onClick}
|
||||
>
|
||||
<span className="aw-ico">{icon}</span>
|
||||
<span className="aw-main">
|
||||
<span className="aw-title">{title}</span>
|
||||
<span className="aw-sub">{sub}</span>
|
||||
</span>
|
||||
<span className="chev">›</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 2 — add-hd (one-tap on an existing seed) ───────────────────────
|
||||
|
||||
function AddHdStep({
|
||||
seeds, selectedSeedId, onSelect, busy, err, onConfirm, onBack,
|
||||
}: {
|
||||
seeds: readonly SeedSummary[];
|
||||
selectedSeedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
busy: boolean;
|
||||
err: string | null;
|
||||
onConfirm: () => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
// Only ONE seed → go straight to confirm; no need to make the user pick from a 1-item list.
|
||||
// Several seeds → render a radio-ish list.
|
||||
return (
|
||||
<div style={{ padding: '4px 2px 0' }}>
|
||||
<div className="ss" style={{ marginBottom: 8 }}>
|
||||
Derives the next BIP-44 account on a seed you already backed up. No new words to write;
|
||||
same recovery phrase restores every account under this seed.
|
||||
</div>
|
||||
{seeds.length > 1 && (
|
||||
<div className="aw-seed-list">
|
||||
{seeds.map((s) => (
|
||||
<button
|
||||
key={s.seedId}
|
||||
className={`aw-seed${selectedSeedId === s.seedId ? ' on' : ''}`}
|
||||
onClick={() => onSelect(s.seedId)}
|
||||
>
|
||||
<span className="aw-ico">🌱</span>
|
||||
<span className="aw-main">
|
||||
<span className="aw-title">{s.label}</span>
|
||||
<span className="aw-sub">{s.walletCount} wallet{s.walletCount === 1 ? '' : 's'} · next account {s.nextAccountIndex}</span>
|
||||
</span>
|
||||
<span className={`chev${selectedSeedId === s.seedId ? ' on' : ''}`}>{selectedSeedId === s.seedId ? '✓' : '○'}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{seeds.length === 1 && (
|
||||
<div className="aw-seed on" style={{ pointerEvents: 'none' }}>
|
||||
<span className="aw-ico">🌱</span>
|
||||
<span className="aw-main">
|
||||
<span className="aw-title">{seeds[0]!.label}</span>
|
||||
<span className="aw-sub">{seeds[0]!.walletCount} wallet{seeds[0]!.walletCount === 1 ? '' : 's'} · next account {seeds[0]!.nextAccountIndex}</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{err && <div className="seed-warn" style={{ marginTop: 8 }}>✗ {err}</div>}
|
||||
<div className="row-btns" style={{ marginTop: 12 }}>
|
||||
<button className="ghost" onClick={onBack}>Back</button>
|
||||
<button
|
||||
className={`cta${busy || !selectedSeedId ? ' disabled' : ''}`}
|
||||
disabled={busy || !selectedSeedId}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{busy ? 'Adding…' : 'Add wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Step 2 — import seed/wif ────────────────────────────────────────────
|
||||
|
||||
function ImportStep({
|
||||
kind, network, secret, setSecret, asMaster, setAsMaster, pin, setPin, busy, err, onConfirm, onBack,
|
||||
}: {
|
||||
kind: 'seed' | 'wif';
|
||||
network: SupportedNetwork;
|
||||
secret: string;
|
||||
setSecret: (v: string) => void;
|
||||
asMaster: boolean;
|
||||
setAsMaster: (v: boolean) => void;
|
||||
pin: string;
|
||||
setPin: (v: string) => void;
|
||||
busy: boolean;
|
||||
err: string | null;
|
||||
onConfirm: () => void;
|
||||
onBack: () => void;
|
||||
}) {
|
||||
const canConfirm = !busy && secret.trim().length > 0 && pin.length >= 4;
|
||||
return (
|
||||
<div style={{ padding: '4px 2px 0' }}>
|
||||
<div className="ss" style={{ marginBottom: 8 }}>
|
||||
{kind === 'seed'
|
||||
? 'Paste 12 or 24 words separated by spaces. The seal uses your app PIN — the same one that unlocks every other wallet.'
|
||||
: 'Paste a WIF-encoded private key. Starts with c on testnet / K or L on mainnet.'}
|
||||
</div>
|
||||
<textarea
|
||||
className="input"
|
||||
rows={kind === 'seed' ? 3 : 2}
|
||||
placeholder={kind === 'seed'
|
||||
? 'orbit spectral ledger nova … (12 or 24 words)'
|
||||
: 'cRA1KXdP2k…'}
|
||||
value={secret}
|
||||
onChange={(e) => setSecret(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{kind === 'seed' && (
|
||||
<label className="ss" style={{ display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' }}>
|
||||
<input type="checkbox" checked={asMaster} onChange={(e) => setAsMaster(e.target.checked)} />
|
||||
Use as a master root — future <b>Add wallet</b> taps derive from it
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
className="input"
|
||||
type="password"
|
||||
placeholder="App PIN"
|
||||
value={pin}
|
||||
onChange={(e) => setPin(e.target.value)}
|
||||
style={{ marginTop: 8 }}
|
||||
/>
|
||||
<div className="ss" style={{ marginTop: 6, opacity: 0.6 }}>
|
||||
Imported on <b>{network}</b> · sealed with the same PIN as your other wallets.
|
||||
</div>
|
||||
{err && <div className="seed-warn" style={{ marginTop: 8 }}>✗ {err}</div>}
|
||||
<div className="row-btns" style={{ marginTop: 12 }}>
|
||||
<button className="ghost" onClick={onBack}>Back</button>
|
||||
<button
|
||||
className={`cta${canConfirm ? '' : ' disabled'}`}
|
||||
disabled={!canConfirm}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{busy ? 'Importing…' : 'Import wallet'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ interface PreviewRow {
|
|||
secret: { seed?: string; wif?: string; path?: string };
|
||||
}
|
||||
|
||||
interface ImportSummary {
|
||||
export interface ImportSummary {
|
||||
ok: number;
|
||||
skipped: number;
|
||||
errors: { slug: string; reason: string }[];
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export function MainScreen({
|
|||
onSign,
|
||||
onViewAgora,
|
||||
onManageWallets,
|
||||
onAddWallet,
|
||||
aggregate = null,
|
||||
activeWalletKey = null,
|
||||
onSwitchWallet,
|
||||
|
|
@ -76,8 +77,13 @@ export function MainScreen({
|
|||
onWalletConnect?: () => void;
|
||||
/** "View in Agora →" from an Assets category card. */
|
||||
onViewAgora?: () => void;
|
||||
/** "+ Add / Import wallet" → Settings → Wallets (add · import · consolidate). */
|
||||
/** The gear icon / "Manage" paths → opens the full Settings screen. */
|
||||
onManageWallets?: () => void;
|
||||
/** "+ Add / Import wallet" button → opens the dedicated step-by-step
|
||||
* AddWalletFlow instead of dumping the whole Settings page. Falls back to
|
||||
* onManageWallets if omitted (preserves the old behaviour on platforms
|
||||
* that don't have the multi-wallet MultiKeyStore). */
|
||||
onAddWallet?: () => void;
|
||||
/** Aggregate balances across every wallet held (0083 all-in-one). null when a
|
||||
* single wallet is all there is. Rows come from the MultiKeyStore set in
|
||||
* production and from the bundled chipnet dev wallets under `__DEV__`. */
|
||||
|
|
@ -381,6 +387,7 @@ export function MainScreen({
|
|||
onSwitchWallet={onSwitchWallet}
|
||||
onRenameWallet={onRenameWallet}
|
||||
onManageWallets={onManageWallets}
|
||||
onAddWallet={onAddWallet}
|
||||
onActivity={onActivity}
|
||||
/>
|
||||
</>
|
||||
|
|
@ -488,7 +495,7 @@ function AssetsPane({
|
|||
// ── Wallets pane — wallet list (show/hide) + per-wallet activity scope ────────
|
||||
function WalletsPane({
|
||||
aggregate, activeWalletKey, activeBchSats, mbaPnvSats, session, aggLoading, displayName, badgeOf,
|
||||
onSwitchWallet, onRenameWallet, onManageWallets, onActivity,
|
||||
onSwitchWallet, onRenameWallet, onManageWallets, onAddWallet, onActivity,
|
||||
}: {
|
||||
aggregate: WalletBalances | null;
|
||||
activeWalletKey: string | null;
|
||||
|
|
@ -501,6 +508,7 @@ function WalletsPane({
|
|||
onSwitchWallet?: (key: string) => void;
|
||||
onRenameWallet?: (key: string, name: string) => void;
|
||||
onManageWallets?: () => void;
|
||||
onAddWallet?: () => void;
|
||||
onActivity?: () => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState<{ key: string; value: string } | null>(null);
|
||||
|
|
@ -616,8 +624,8 @@ function WalletsPane({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
{onManageWallets && (
|
||||
<button className="add-cta" onClick={onManageWallets}>+ Add / Import wallet</button>
|
||||
{(onAddWallet || onManageWallets) && (
|
||||
<button className="add-cta" onClick={onAddWallet ?? onManageWallets}>+ Add / Import wallet</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -981,3 +981,44 @@ body.barcode-scanner-active .modal.barcode-scanner-modal .flow-head {
|
|||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* ─── AddWalletFlow — focused add/import wallet wizard ──────────────────────
|
||||
Step 1 lists the four add paths as tappable cards. Step 2 is a focused
|
||||
form (seed-list, or seed/WIF + PIN inputs). The flow sits on the standard
|
||||
.modal / .sheet-backdrop pattern used by SendFlow/SweepFlow — no new shell. */
|
||||
.aw-paths { display: flex; flex-direction: column; gap: 10px; padding: 4px 2px 0; }
|
||||
.aw-path {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 14px; border-radius: 12px;
|
||||
background: var(--surface-1); border: 1px solid var(--border);
|
||||
color: var(--ink); text-align: left; cursor: pointer; width: 100%;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
}
|
||||
.aw-path:hover:not(.disabled) { border-color: var(--acid); background: rgba(214, 255, 61, 0.04); }
|
||||
.aw-path.disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
.aw-path .aw-ico {
|
||||
font-size: 20px; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;
|
||||
flex: none; color: var(--acid);
|
||||
}
|
||||
.aw-path .aw-main { display: flex; flex-direction: column; flex: 1; gap: 2px; min-width: 0; }
|
||||
.aw-path .aw-title { font-size: 14px; font-weight: 500; color: var(--ink); }
|
||||
.aw-path .aw-sub { font-size: 11.5px; color: var(--muted); line-height: 1.4; }
|
||||
.aw-path .chev { color: var(--muted); font-size: 16px; flex: none; }
|
||||
|
||||
/* Step 2 seed list (when more than one master seed exists). */
|
||||
.aw-seed-list { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
|
||||
.aw-seed {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 12px; border-radius: 10px;
|
||||
background: var(--surface-1); border: 1px solid var(--border);
|
||||
color: var(--ink); text-align: left; cursor: pointer; width: 100%;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.aw-seed:hover { border-color: var(--acid); }
|
||||
.aw-seed.on { border-color: var(--acid); background: rgba(214, 255, 61, 0.06); }
|
||||
.aw-seed .aw-ico { font-size: 18px; width: 26px; flex: none; }
|
||||
.aw-seed .aw-main { display: flex; flex-direction: column; flex: 1; gap: 2px; min-width: 0; }
|
||||
.aw-seed .aw-title { font-size: 13.5px; color: var(--ink); }
|
||||
.aw-seed .aw-sub { font-size: 11px; color: var(--muted); }
|
||||
.aw-seed .chev { color: var(--muted); flex: none; }
|
||||
.aw-seed .chev.on { color: var(--acid); }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue