-
-
- {account.name}
-
+
+
)
}
@@ -188,9 +177,9 @@ const MainContentBody = ({
hideAssetsBelowOneToggle?: HideAssetsBelowOneToggle
}) => {
const { account, isLoading } = usePortfolio()
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const { currentAccount, isLoading: isWalletLoading } = useWallet()
- const address = currentWallet?.activeAccounts[0].address
+ const address = currentAccount?.address
if (isLoading || isWalletLoading) {
return (
diff --git a/apps/wallet/src/components/wallet-selector.tsx b/apps/wallet/src/components/wallet-selector.tsx
new file mode 100644
index 000000000..1ad64885e
--- /dev/null
+++ b/apps/wallet/src/components/wallet-selector.tsx
@@ -0,0 +1,99 @@
+import { Avatar, Button, DropdownMenu /*Tooltip*/ } from '@status-im/components'
+import {
+ AddIcon,
+ ChevronDownIcon,
+ ImportIcon,
+ WalletIcon,
+} from '@status-im/icons/20'
+// import { shortenAddress } from '@status-im/wallet/components'
+import { useNavigate } from '@tanstack/react-router'
+
+import { useWallet } from '@/providers/wallet-context'
+
+type Props = {
+ className?: string
+}
+
+const DEFAULT_ACCOUNT_EMOJI = '🍑'
+const DEFAULT_ACCOUNT_NAME = 'Account 1'
+
+export function WalletSelector(props: Props) {
+ const { className } = props
+ const navigate = useNavigate()
+ const { wallets, currentWallet /*, currentAccount*/, setCurrentWallet } =
+ useWallet()
+
+ if (!currentWallet) {
+ return null
+ }
+
+ return (
+
+
+
+
+
+ {currentWallet.name}
+
+ {/* TODO: Uncomment to display current account's name
+ when multi-account support is implemented */}
+ {/* {currentAccount?.address ? (
+
+
+ {shortenAddress(currentAccount.address)}
+
+
+ ) : null} */}
+
+
+
+
+ }
+ aria-label="Open wallet menu"
+ />
+
+
+ Wallets
+ {wallets.map(wallet => (
+ }
+ label={wallet.name}
+ selected={wallet.id === currentWallet.id}
+ onClick={() => setCurrentWallet(wallet.id)}
+ />
+ ))}
+
+ }
+ label="Create wallet"
+ onClick={() => {
+ navigate({ to: '/wallet-flow/new' })
+ }}
+ />
+ }
+ label="Import wallet"
+ onClick={() => {
+ navigate({ to: '/wallet-flow/import' })
+ }}
+ />
+
+
+
+ )
+}
diff --git a/apps/wallet/src/data/api.ts b/apps/wallet/src/data/api.ts
index b1e0f3330..bdc7b1d18 100644
--- a/apps/wallet/src/data/api.ts
+++ b/apps/wallet/src/data/api.ts
@@ -87,10 +87,17 @@ const apiRouter = router({
// todo: words count option
// todo: handle cancelation
add: procedure
- .input(z.object({ password: z.string(), name: z.string() }))
+ .input(
+ z.object({
+ password: z.string().optional(),
+ name: z.string().optional(),
+ }),
+ )
.mutation(async ({ input, ctx }) => {
const { walletCore, session } = ctx
- const hd = walletCore.HDWallet.create(128, input.password)
+ const walletCount = (await walletMetadata.getAll()).length
+ const walletName = input.name ?? `Wallet ${walletCount + 1}`
+ const hd = walletCore.HDWallet.create(128, input.password ?? '')
const mnemonic = hd.mnemonic()
hd.delete()
const id = crypto.randomUUID()
@@ -104,6 +111,9 @@ const apiRouter = router({
if (await hasVault()) {
await session.addWalletToVault(id, 'mnemonic', mnemonic)
} else {
+ if (!input.password) {
+ throw new Error('Password is required to create the first wallet')
+ }
await encryptAndStore(input.password, {
wallets: { [id]: { type: 'mnemonic', secret: mnemonic } },
})
@@ -111,13 +121,15 @@ const apiRouter = router({
}
await walletMetadata.save({
id,
- name: input.name,
+ name: walletName,
type: 'mnemonic',
- activeAccounts: [account],
+ accounts: [account],
+ selectedAccountAddress: account.address,
})
return {
// note: reference and store accounts with
id,
+ name: walletName,
mnemonic,
}
}),
@@ -142,12 +154,14 @@ const apiRouter = router({
.input(
z.object({
mnemonic: z.string(),
- name: z.string(),
- password: z.string(),
+ name: z.string().optional(),
+ password: z.string().optional(),
}),
)
.mutation(async ({ input, ctx }) => {
const { walletCore, session } = ctx
+ const walletCount = (await walletMetadata.getAll()).length
+ const walletName = input.name ?? `Wallet ${walletCount + 1}`
const id = crypto.randomUUID()
const account = deriveAccount(
walletCore,
@@ -159,6 +173,9 @@ const apiRouter = router({
if (await hasVault()) {
await session.addWalletToVault(id, 'mnemonic', input.mnemonic)
} else {
+ if (!input.password) {
+ throw new Error('Password is required to import the first wallet')
+ }
await encryptAndStore(input.password, {
wallets: { [id]: { type: 'mnemonic', secret: input.mnemonic } },
})
@@ -166,11 +183,12 @@ const apiRouter = router({
}
await walletMetadata.save({
id,
- name: input.name,
+ name: walletName,
type: 'mnemonic',
- activeAccounts: [account],
+ accounts: [account],
+ selectedAccountAddress: account.address,
})
- return { id, mnemonic: input.mnemonic }
+ return { id, name: walletName, mnemonic: input.mnemonic }
}),
account: router({
@@ -179,7 +197,7 @@ const apiRouter = router({
.query(async ({ input }) => {
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- return wallet.activeAccounts
+ return wallet.accounts
}),
ethereum: router({
@@ -196,7 +214,7 @@ const apiRouter = router({
if (!wallet) throw new Error('Wallet not found')
const mnemonic = await ctx.session.getMnemonic(input.walletId)
const index = deriveNextAccountIndex(
- wallet.activeAccounts,
+ wallet.accounts,
walletCore.CoinType.ethereum.value,
)
const account = deriveAccount(
@@ -227,7 +245,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -272,7 +290,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -318,7 +336,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -360,7 +378,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -405,7 +423,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -493,7 +511,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -528,7 +546,7 @@ const apiRouter = router({
if (!wallet) throw new Error('Wallet not found')
const mnemonic = await ctx.session.getMnemonic(input.walletId)
const index = deriveNextAccountIndex(
- wallet.activeAccounts,
+ wallet.accounts,
walletCore.CoinType.solana.value,
)
const account = deriveAccount(
@@ -556,7 +574,7 @@ const apiRouter = router({
const { walletCore } = ctx
const wallet = await walletMetadata.get(input.walletId)
if (!wallet) throw new Error('Wallet not found')
- const account = wallet.activeAccounts.find(
+ const account = wallet.accounts.find(
a => a.address === input.fromAddress,
)
if (!account) throw new Error('From address not found')
@@ -592,7 +610,7 @@ const apiRouter = router({
const mnemonic = await ctx.session.getMnemonic(input.walletId)
const index = deriveNextAccountIndex(
- wallet.activeAccounts,
+ wallet.accounts,
walletCore.CoinType.cardano.value,
)
const account = deriveAccount(
@@ -614,12 +632,14 @@ const apiRouter = router({
.input(
z.object({
privateKey: z.string(),
- name: z.string(),
+ name: z.string().optional(),
password: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
const { walletCore, session } = ctx
+ const walletCount = (await walletMetadata.getAll()).length
+ const walletName = input.name ?? `Wallet ${walletCount + 1}`
const hex = input.privateKey.replace(/^0x/, '')
const keyBytes = walletCore.HexCoding.decode(hex)
const pk = walletCore.PrivateKey.createWithData(keyBytes)
@@ -645,9 +665,10 @@ const apiRouter = router({
}
await walletMetadata.save({
id,
- name: input.name,
+ name: walletName,
type: 'privateKey',
- activeAccounts: [account],
+ accounts: [account],
+ selectedAccountAddress: account.address,
})
return {
// reference stored (single) account
diff --git a/apps/wallet/src/data/session.ts b/apps/wallet/src/data/session.ts
index f63462c65..89f0eb3c5 100644
--- a/apps/wallet/src/data/session.ts
+++ b/apps/wallet/src/data/session.ts
@@ -66,19 +66,18 @@ async function migrateFromLegacy(
try {
const secret = (await keyStore.export(w.id, password)) as string
secrets.wallets[w.id] = { type: 'mnemonic', secret }
- const activeAccounts: WalletAccount[] = (w.activeAccounts ?? []).map(
- a => ({
- address: a.address,
- coin: a.coin ?? 60,
- derivationPath: a.derivationPath ?? FALLBACK_DERIVATION_PATH,
- derivation: a.derivation ?? 0,
- }),
- )
+ const accounts: WalletAccount[] = (w.activeAccounts ?? []).map(a => ({
+ address: a.address,
+ coin: a.coin ?? 60,
+ derivationPath: a.derivationPath ?? FALLBACK_DERIVATION_PATH,
+ derivation: a.derivation ?? 0,
+ }))
await walletMetadata.save({
id: w.id,
name: w.name,
type: 'mnemonic',
- activeAccounts,
+ accounts,
+ selectedAccountAddress: accounts[0]?.address,
})
await keyStore.delete(w.id, password)
} catch (err) {
diff --git a/apps/wallet/src/data/wallet-metadata.ts b/apps/wallet/src/data/wallet-metadata.ts
index d3826cc3d..68c823163 100644
--- a/apps/wallet/src/data/wallet-metadata.ts
+++ b/apps/wallet/src/data/wallet-metadata.ts
@@ -13,10 +13,75 @@ export type WalletMeta = {
id: string
name: string
type: 'mnemonic' | 'privateKey'
- activeAccounts: WalletAccount[]
+ accounts: WalletAccount[]
+ selectedAccountAddress?: string
}
-type MetadataStore = Record
+// TODO: remove LegacyWalletMeta once no user has legacy wallets; use WalletMeta directly in MetadataStore
+type LegacyWalletMeta = {
+ id: string
+ name: string
+ type: 'mnemonic' | 'privateKey'
+ activeAccounts?: WalletAccount[]
+ accounts?: WalletAccount[]
+ selectedAccountAddress?: string
+}
+
+type MetadataStore = Record
+
+// TODO: remove LEGACY_WALLET_NAME, WALLET_NAME_REGEX, and renameLegacyWallets once no user has legacy wallets
+const LEGACY_WALLET_NAME = 'Account 1'
+// Matches "Wallet 1", "Wallet 42", etc. & captures the trailing integer
+const WALLET_NAME_REGEX = /^Wallet (\d+)$/
+
+// TODO: remove isNormalized and normalizeWallet once no user has legacy wallets
+function isNormalized(wallet: LegacyWalletMeta): wallet is WalletMeta {
+ return wallet.accounts !== undefined && wallet.activeAccounts === undefined
+}
+
+function normalizeWallet(wallet: LegacyWalletMeta): WalletMeta {
+ if (isNormalized(wallet)) return wallet
+
+ const accounts = wallet.accounts ?? wallet.activeAccounts ?? []
+ const selectedAccountAddress = accounts.some(
+ account => account.address === wallet.selectedAccountAddress,
+ )
+ ? wallet.selectedAccountAddress
+ : accounts[0]?.address
+
+ return {
+ id: wallet.id,
+ name: wallet.name,
+ type: wallet.type,
+ accounts,
+ selectedAccountAddress,
+ }
+}
+
+function renameLegacyWallets(wallets: WalletMeta[]): WalletMeta[] {
+ const legacyCount = wallets.filter(w => w.name === LEGACY_WALLET_NAME).length
+ if (legacyCount === 0) return wallets
+
+ // Collect integers already used by "Wallet N" names to avoid collisions
+ const takenNumbers = new Set()
+ for (const w of wallets) {
+ const match = WALLET_NAME_REGEX.exec(w.name)
+ if (match) takenNumbers.add(Number(match[1]))
+ }
+
+ // Pre-compute exactly as many free integers as there are legacy wallets
+ const available: number[] = []
+ for (let n = 1; available.length < legacyCount; n++) {
+ if (!takenNumbers.has(n)) available.push(n)
+ }
+
+ // Assign each legacy wallet the next free "Wallet N" name in order
+ let idx = 0
+ return wallets.map(w => {
+ if (w.name !== LEGACY_WALLET_NAME) return w
+ return { ...w, name: `Wallet ${available[idx++]}` }
+ })
+}
async function getStore(): Promise {
const data = await storage.getItem(VAULT_METADATA_KEY)
@@ -30,17 +95,27 @@ async function setStore(store: MetadataStore): Promise {
export async function getAll(): Promise {
const store = await getStore()
- return Object.values(store)
+ const wallets = Object.values(store).map(normalizeWallet)
+ const renamed = renameLegacyWallets(wallets)
+
+ // Persist renames so legacy names are updated once and not re-computed every read
+ if (renamed !== wallets) {
+ const updated: MetadataStore = {}
+ for (const w of renamed) updated[w.id] = w
+ await setStore(updated)
+ }
+
+ return renamed
}
export async function get(walletId: string): Promise {
- const store = await getStore()
- return store[walletId] ?? null
+ const all = await getAll()
+ return all.find(w => w.id === walletId) ?? null
}
export async function save(wallet: WalletMeta): Promise {
const store = await getStore()
- store[wallet.id] = wallet
+ store[wallet.id] = normalizeWallet(wallet)
await setStore(store)
}
@@ -50,8 +125,11 @@ export async function addAccount(
): Promise {
const wallet = await get(walletId)
if (!wallet) throw new Error(`Wallet ${walletId} not found`)
- if (wallet.activeAccounts.some(a => a.address === account.address)) return
- wallet.activeAccounts.push(account)
+ if (wallet.accounts.some(a => a.address === account.address)) return
+ wallet.accounts.push(account)
+ if (!wallet.selectedAccountAddress) {
+ wallet.selectedAccountAddress = account.address
+ }
await save(wallet)
}
diff --git a/apps/wallet/src/hooks/use-create-wallet.ts b/apps/wallet/src/hooks/use-create-wallet.ts
index ad22792eb..dbd4c583b 100644
--- a/apps/wallet/src/hooks/use-create-wallet.ts
+++ b/apps/wallet/src/hooks/use-create-wallet.ts
@@ -10,12 +10,12 @@ export const useCreateWallet = () => {
const { mutate, mutateAsync, ...result } = useMutation({
mutationKey: ['create-wallet'],
- mutationFn: async (password: string) => {
- await api.wallet.add.mutate({
+ mutationFn: async (password?: string) => {
+ const createdWallet = await api.wallet.add.mutate({
password,
- name: 'Account 1',
})
await markAsNeedsBackup()
+ return createdWallet
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['wallets'] })
diff --git a/apps/wallet/src/hooks/use-import-wallet.ts b/apps/wallet/src/hooks/use-import-wallet.ts
index 5bedae32e..cb0087c50 100644
--- a/apps/wallet/src/hooks/use-import-wallet.ts
+++ b/apps/wallet/src/hooks/use-import-wallet.ts
@@ -13,12 +13,11 @@ export const useImportWallet = () => {
password,
}: {
mnemonic: string
- password: string
+ password?: string
}) => {
- await api.wallet.import.mutate({
+ return api.wallet.import.mutate({
mnemonic,
password,
- name: 'Account 1',
})
},
onSuccess: () => {
diff --git a/apps/wallet/src/hooks/use-portfolio.ts b/apps/wallet/src/hooks/use-portfolio.ts
index 656850440..99ddaada2 100644
--- a/apps/wallet/src/hooks/use-portfolio.ts
+++ b/apps/wallet/src/hooks/use-portfolio.ts
@@ -9,22 +9,27 @@ const DEFAULT_SUMMARY = {
}
const MOCK_ACCOUNT = {
- name: 'Account 1',
emoji: '🍑',
color: 'magenta',
+ name: 'Account 1',
}
const usePortfolio = () => {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const { data: assetsData, isLoading: isAssetsLoading } = useAssets({
- address: currentWallet?.activeAccounts[0]?.address,
+ address: currentAccount?.address,
isWalletLoading,
})
const account = {
...MOCK_ACCOUNT,
- name: currentWallet?.name || MOCK_ACCOUNT.name,
+ // TODO: Use currently selected account name instead when multi-account support is implemented.
+ name: currentWallet?.name ?? MOCK_ACCOUNT.name,
}
const summary = assetsData?.summary || DEFAULT_SUMMARY
diff --git a/apps/wallet/src/hooks/use-wallet-flow-success.ts b/apps/wallet/src/hooks/use-wallet-flow-success.ts
new file mode 100644
index 000000000..3f479cb51
--- /dev/null
+++ b/apps/wallet/src/hooks/use-wallet-flow-success.ts
@@ -0,0 +1,20 @@
+import { useToast } from '@status-im/components'
+import { useQueryClient } from '@tanstack/react-query'
+import { useNavigate } from '@tanstack/react-router'
+
+import { useWallet } from '@/providers/wallet-context'
+
+export function useWalletFlowSuccess(successHref: string) {
+ const { setCurrentWallet } = useWallet()
+ const queryClient = useQueryClient()
+ const navigate = useNavigate()
+ const toast = useToast()
+
+ return async (wallet: { id: string; name: string }, message: string) => {
+ await queryClient.invalidateQueries({ queryKey: ['wallets'] })
+ setCurrentWallet(wallet.id)
+ toast.positive(message)
+ await queryClient.invalidateQueries({ queryKey: ['session', 'status'] })
+ navigate({ to: successHref })
+ }
+}
diff --git a/apps/wallet/src/lib/rpc-handler.ts b/apps/wallet/src/lib/rpc-handler.ts
index 37dfe67cc..f2fcbe5e0 100644
--- a/apps/wallet/src/lib/rpc-handler.ts
+++ b/apps/wallet/src/lib/rpc-handler.ts
@@ -18,6 +18,7 @@ const publicClient = createPublicClient({
const DEFAULT_CHAIN_ID = '0x1'
const SUPPORTED_CHAIN_IDS = new Set(['0x1', '0x6300b5ea'])
+const DEFAULT_ACCOUNT_NAME = 'Account 1'
// 5 minutes in ms
const APPROVAL_TIMEOUT_MS = 5 * 60 * 1000
@@ -47,7 +48,7 @@ async function getAddress(): Promise {
async function getAccountName(): Promise {
const result = await chrome.storage.session.get('dappAccountName')
- return (result.dappAccountName as string) || 'Account 1'
+ return (result.dappAccountName as string) || DEFAULT_ACCOUNT_NAME
}
async function getWalletId(): Promise {
diff --git a/apps/wallet/src/providers/signer-context.tsx b/apps/wallet/src/providers/signer-context.tsx
index 44091ce60..8f653c75a 100644
--- a/apps/wallet/src/providers/signer-context.tsx
+++ b/apps/wallet/src/providers/signer-context.tsx
@@ -32,6 +32,8 @@ type SignerContextValue = {
requestUnlock: () => Promise
}
+const DEFAULT_ACCOUNT_NAME = 'Account 1'
+
const SignerContext = createContext(undefined)
export function useWalletSigner() {
@@ -43,13 +45,17 @@ export function useWalletSigner() {
}
export function SignerProvider({ children }: { children: React.ReactNode }) {
- const { currentWallet } = useWallet()
+ const { currentWallet, currentAccount } = useWallet()
const { hasActiveSession, requestPassword, clearSession } = usePassword()
- const address = currentWallet?.activeAccounts[0]?.address as
- | Address
- | undefined
- const accountName = currentWallet?.name ?? 'Account 1'
+ const address = useMemo(() => {
+ return currentAccount?.address as Address | undefined
+ }, [currentAccount])
+
+ const accountName = useMemo(() => {
+ // TODO: Use currently selected account name instead when multi-account support is implemented.
+ return currentWallet?.name ?? DEFAULT_ACCOUNT_NAME
+ }, [currentWallet])
useEffect(() => {
if (address) {
diff --git a/apps/wallet/src/providers/wallet-context.tsx b/apps/wallet/src/providers/wallet-context.tsx
index 5cbfdccfb..72aecd2f9 100644
--- a/apps/wallet/src/providers/wallet-context.tsx
+++ b/apps/wallet/src/providers/wallet-context.tsx
@@ -8,19 +8,22 @@ import {
} from 'react'
import { useQuery } from '@tanstack/react-query'
+import { storage } from '@wxt-dev/storage'
import { useSynchronizedRefetch } from '../hooks/use-synchronized-refetch'
import { apiClient } from './api-client'
-import type { WalletMeta } from '../data/wallet-metadata'
+import type { WalletAccount, WalletMeta } from '../data/wallet-metadata'
const WALLET_LIST_STALE_TIME_MS = 5 * 60 * 1000 // 5 minutes
const WALLET_LIST_GC_TIME_MS = 60 * 60 * 1000 // 1 hour
+const SELECTED_WALLET_ID_KEY = 'local:wallet:selected-id'
type Wallet = WalletMeta
type WalletContext = {
currentWallet: Wallet | null
+ currentAccount: WalletAccount | null
wallets: Wallet[]
isLoading: boolean
hasWallets: boolean
@@ -39,6 +42,8 @@ export function useWallet() {
export function WalletProvider({ children }: { children: React.ReactNode }) {
const [selectedWalletId, setSelectedWalletId] = useState(null)
+ const [hasHydratedSelectedWallet, setHasHydratedSelectedWallet] =
+ useState(false)
const { data: wallets = [], isLoading } = useQuery({
queryKey: ['wallets'],
@@ -62,29 +67,73 @@ export function WalletProvider({ children }: { children: React.ReactNode }) {
return wallets[0] || null
}, [hasWallets, selectedWalletId, wallets])
+ const currentAccount = useMemo(() => {
+ if (!currentWallet) return null
+ // TODO: Use currently selected account when multi-account support is implemented. See ^^^
+ return currentWallet.accounts[0] ?? null
+ }, [currentWallet])
+
useEffect(() => {
- if (hasWallets && !selectedWalletId && wallets[0]) {
+ if (
+ hasWallets &&
+ !selectedWalletId &&
+ wallets[0] &&
+ hasHydratedSelectedWallet
+ ) {
setSelectedWalletId(wallets[0].id)
}
- }, [hasWallets, selectedWalletId, wallets])
+ }, [hasHydratedSelectedWallet, hasWallets, selectedWalletId, wallets])
- const setCurrentWallet = useCallback(
- (id: string) => {
- const walletExists = wallets.some(w => w.id === id)
- if (walletExists) {
- setSelectedWalletId(id)
- } else {
- console.error(`Wallet with id ${id} not found`)
+ useEffect(() => {
+ let isCancelled = false
+
+ async function hydrateSelectedWallet() {
+ const persistedSelectedWalletId = await storage.getItem(
+ SELECTED_WALLET_ID_KEY,
+ )
+ if (isCancelled) return
+ if (persistedSelectedWalletId) {
+ setSelectedWalletId(persistedSelectedWalletId)
}
- },
- [wallets],
- )
+ setHasHydratedSelectedWallet(true)
+ }
+
+ hydrateSelectedWallet().catch(error => {
+ console.error('Failed to hydrate selected wallet id:', error)
+ setHasHydratedSelectedWallet(true)
+ })
+
+ return () => {
+ isCancelled = true
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!hasHydratedSelectedWallet) return
+
+ const persistSelectedWallet = async () => {
+ if (selectedWalletId) {
+ await storage.setItem(SELECTED_WALLET_ID_KEY, selectedWalletId)
+ return
+ }
+ await storage.removeItem(SELECTED_WALLET_ID_KEY)
+ }
+
+ persistSelectedWallet().catch(error =>
+ console.error('Failed to persist selected wallet id:', error),
+ )
+ }, [hasHydratedSelectedWallet, selectedWalletId])
+
+ const setCurrentWallet = useCallback((id: string) => {
+ setSelectedWalletId(id)
+ }, [])
// Auto-refresh
- useSynchronizedRefetch(currentWallet?.activeAccounts[0]?.address ?? '')
+ useSynchronizedRefetch(currentAccount?.address ?? '')
const contextValue: WalletContext = {
currentWallet,
+ currentAccount,
wallets,
isLoading,
hasWallets,
diff --git a/apps/wallet/src/router.gen.ts b/apps/wallet/src/router.gen.ts
index 13245845e..a81f2ef90 100644
--- a/apps/wallet/src/router.gen.ts
+++ b/apps/wallet/src/router.gen.ts
@@ -12,6 +12,8 @@ import { Route as rootRouteImport } from './routes/__root'
import { Route as OnboardingLayoutRouteImport } from './routes/onboarding/_layout'
import { Route as IndexRouteImport } from './routes/index'
import { Route as OnboardingIndexRouteImport } from './routes/onboarding/index'
+import { Route as WalletFlowNewRouteImport } from './routes/wallet-flow/new'
+import { Route as WalletFlowImportRouteImport } from './routes/wallet-flow/import'
import { Route as OnboardingNewRouteImport } from './routes/onboarding/new'
import { Route as OnboardingImportRouteImport } from './routes/onboarding/import'
import { Route as PortfolioCollectiblesIndexRouteImport } from './routes/portfolio/collectibles/index'
@@ -35,6 +37,16 @@ const OnboardingIndexRoute = OnboardingIndexRouteImport.update({
path: '/',
getParentRoute: () => OnboardingLayoutRoute,
} as any)
+const WalletFlowNewRoute = WalletFlowNewRouteImport.update({
+ id: '/wallet-flow/new',
+ path: '/wallet-flow/new',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const WalletFlowImportRoute = WalletFlowImportRouteImport.update({
+ id: '/wallet-flow/import',
+ path: '/wallet-flow/import',
+ getParentRoute: () => rootRouteImport,
+} as any)
const OnboardingNewRoute = OnboardingNewRouteImport.update({
id: '/new',
path: '/new',
@@ -78,6 +90,8 @@ export interface FileRoutesByFullPath {
'/onboarding': typeof OnboardingLayoutRouteWithChildren
'/onboarding/import': typeof OnboardingImportRoute
'/onboarding/new': typeof OnboardingNewRoute
+ '/wallet-flow/import': typeof WalletFlowImportRoute
+ '/wallet-flow/new': typeof WalletFlowNewRoute
'/onboarding/': typeof OnboardingIndexRoute
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
'/portfolio/activity': typeof PortfolioActivityIndexRoute
@@ -89,6 +103,8 @@ export interface FileRoutesByTo {
'/': typeof IndexRoute
'/onboarding/import': typeof OnboardingImportRoute
'/onboarding/new': typeof OnboardingNewRoute
+ '/wallet-flow/import': typeof WalletFlowImportRoute
+ '/wallet-flow/new': typeof WalletFlowNewRoute
'/onboarding': typeof OnboardingIndexRoute
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
'/portfolio/activity': typeof PortfolioActivityIndexRoute
@@ -102,6 +118,8 @@ export interface FileRoutesById {
'/onboarding': typeof OnboardingLayoutRouteWithChildren
'/onboarding/import': typeof OnboardingImportRoute
'/onboarding/new': typeof OnboardingNewRoute
+ '/wallet-flow/import': typeof WalletFlowImportRoute
+ '/wallet-flow/new': typeof WalletFlowNewRoute
'/onboarding/': typeof OnboardingIndexRoute
'/portfolio/assets/$ticker': typeof PortfolioAssetsTickerRoute
'/portfolio/activity/': typeof PortfolioActivityIndexRoute
@@ -116,6 +134,8 @@ export interface FileRouteTypes {
| '/onboarding'
| '/onboarding/import'
| '/onboarding/new'
+ | '/wallet-flow/import'
+ | '/wallet-flow/new'
| '/onboarding/'
| '/portfolio/assets/$ticker'
| '/portfolio/activity'
@@ -127,6 +147,8 @@ export interface FileRouteTypes {
| '/'
| '/onboarding/import'
| '/onboarding/new'
+ | '/wallet-flow/import'
+ | '/wallet-flow/new'
| '/onboarding'
| '/portfolio/assets/$ticker'
| '/portfolio/activity'
@@ -139,6 +161,8 @@ export interface FileRouteTypes {
| '/onboarding'
| '/onboarding/import'
| '/onboarding/new'
+ | '/wallet-flow/import'
+ | '/wallet-flow/new'
| '/onboarding/'
| '/portfolio/assets/$ticker'
| '/portfolio/activity/'
@@ -150,6 +174,8 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
OnboardingLayoutRoute: typeof OnboardingLayoutRouteWithChildren
+ WalletFlowImportRoute: typeof WalletFlowImportRoute
+ WalletFlowNewRoute: typeof WalletFlowNewRoute
PortfolioAssetsTickerRoute: typeof PortfolioAssetsTickerRoute
PortfolioActivityIndexRoute: typeof PortfolioActivityIndexRoute
PortfolioAssetsIndexRoute: typeof PortfolioAssetsIndexRoute
@@ -180,6 +206,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof OnboardingIndexRouteImport
parentRoute: typeof OnboardingLayoutRoute
}
+ '/wallet-flow/new': {
+ id: '/wallet-flow/new'
+ path: '/wallet-flow/new'
+ fullPath: '/wallet-flow/new'
+ preLoaderRoute: typeof WalletFlowNewRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/wallet-flow/import': {
+ id: '/wallet-flow/import'
+ path: '/wallet-flow/import'
+ fullPath: '/wallet-flow/import'
+ preLoaderRoute: typeof WalletFlowImportRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/onboarding/new': {
id: '/onboarding/new'
path: '/new'
@@ -250,6 +290,8 @@ const OnboardingLayoutRouteWithChildren =
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
OnboardingLayoutRoute: OnboardingLayoutRouteWithChildren,
+ WalletFlowImportRoute: WalletFlowImportRoute,
+ WalletFlowNewRoute: WalletFlowNewRoute,
PortfolioAssetsTickerRoute: PortfolioAssetsTickerRoute,
PortfolioActivityIndexRoute: PortfolioActivityIndexRoute,
PortfolioAssetsIndexRoute: PortfolioAssetsIndexRoute,
diff --git a/apps/wallet/src/routes/__root.tsx b/apps/wallet/src/routes/__root.tsx
index 6b73959ec..3d962c56c 100644
--- a/apps/wallet/src/routes/__root.tsx
+++ b/apps/wallet/src/routes/__root.tsx
@@ -48,6 +48,10 @@ export const Route = createRootRouteWithContext<{
throw redirect({ to: '/onboarding' })
}
+ if (location.pathname.startsWith('/wallet-flow') && !hasWallets) {
+ throw redirect({ to: '/onboarding' })
+ }
+
if (location.pathname.startsWith('/onboarding') && hasWallets) {
throw redirect({ to: '/portfolio/assets' })
}
diff --git a/apps/wallet/src/routes/onboarding/_layout.tsx b/apps/wallet/src/routes/onboarding/_layout.tsx
index 527e92da8..6a4b17e70 100644
--- a/apps/wallet/src/routes/onboarding/_layout.tsx
+++ b/apps/wallet/src/routes/onboarding/_layout.tsx
@@ -1,33 +1,15 @@
-import { BlurredCircle } from '@status-im/wallet/components'
import { createFileRoute, Outlet } from '@tanstack/react-router'
+import { OnboardingFlowLayout } from '@/components/onboarding/flow-layout'
+
export const Route = createFileRoute('/onboarding')({
component: RouteComponent,
})
function RouteComponent() {
return (
-
+
+
+
)
}
diff --git a/apps/wallet/src/routes/onboarding/import.tsx b/apps/wallet/src/routes/onboarding/import.tsx
index 5a8ae84b0..a7ad8122c 100644
--- a/apps/wallet/src/routes/onboarding/import.tsx
+++ b/apps/wallet/src/routes/onboarding/import.tsx
@@ -1,164 +1,13 @@
-import { useState, useTransition } from 'react'
+import { createFileRoute } from '@tanstack/react-router'
-import { Button, Text } from '@status-im/components'
-import { ArrowLeftIcon } from '@status-im/icons/20'
-import {
- CreatePasswordForm,
- type CreatePasswordFormValues,
- ImportRecoveryPhraseForm,
- type ImportRecoveryPhraseFormValues,
-} from '@status-im/wallet/components'
-import { useQueryClient } from '@tanstack/react-query'
-import { createFileRoute, useNavigate } from '@tanstack/react-router'
-
-import { useImportWallet } from '../../hooks/use-import-wallet'
-
-import type { SubmitHandler } from 'react-hook-form'
+import { ImportWalletFlow } from '@/components/onboarding/import-wallet-flow'
export const Route = createFileRoute('/onboarding/import')({
component: RouteComponent,
})
-type OnboardingState =
- | {
- type: 'import-wallet'
- mnemonic: string
- }
- | {
- type: 'create-password'
- mnemonic: string
- }
-
function RouteComponent() {
- const [onboardingState, setOnboardingState] = useState({
- type: 'import-wallet',
- mnemonic: '',
- })
-
- return (
-
- {onboardingState.type === 'import-wallet' && (
- {
- setOnboardingState({ type: 'create-password', mnemonic })
- }}
- mnemonic={onboardingState.mnemonic}
- />
- )}
- {onboardingState.type === 'create-password' && (
-
- setOnboardingState({
- type: 'import-wallet',
- mnemonic,
- })
- }
- />
- )}
-
- )
-}
-
-function ImportWallet({
- onNext,
- mnemonic,
-}: {
- onNext: (mnemonic: string) => void
- mnemonic: string
-}) {
- const [isPending, startTransition] = useTransition()
-
- const onSubmit: SubmitHandler<
- ImportRecoveryPhraseFormValues
- > = async data => {
- try {
- startTransition(() => {
- onNext(data.mnemonic)
- })
- } catch (error) {
- console.error(error)
- }
- }
-
return (
-
-
- }
- aria-label="Back"
- size="32"
- />
-
-
- Import via recovery phrase
-
-
- Type or paste your 12, 15, 18, 21 or 24 words Ethereum recovery phrase
-
-
-
-
- )
-}
-
-function CreatePassword({
- mnemonic,
- onBack,
-}: {
- mnemonic: string
- onBack: (mnemonic: string) => void
-}) {
- const { importWalletAsync } = useImportWallet()
- const queryClient = useQueryClient()
- const navigate = useNavigate()
- const [isLoading, setIsLoading] = useState(false)
-
- const handleSubmit: SubmitHandler = async data => {
- setIsLoading(true)
- try {
- await importWalletAsync({
- mnemonic,
- password: data.password,
- })
- await queryClient.invalidateQueries({ queryKey: ['session', 'status'] })
- navigate({ to: '/portfolio/assets' })
- } catch (error) {
- console.error(error)
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
-
-
-
-
-
Create password
-
- To unlock the extension and sign transactions, the password is stored
- only on your device. Status can't recover it.
-
-
-
-
+
)
}
diff --git a/apps/wallet/src/routes/onboarding/new.tsx b/apps/wallet/src/routes/onboarding/new.tsx
index 1ee76c4b6..f5adc2ae1 100644
--- a/apps/wallet/src/routes/onboarding/new.tsx
+++ b/apps/wallet/src/routes/onboarding/new.tsx
@@ -1,59 +1,13 @@
-import { useState } from 'react'
+import { createFileRoute } from '@tanstack/react-router'
-import { Button } from '@status-im/components'
-import { ArrowLeftIcon } from '@status-im/icons/20'
-import { CreatePasswordForm } from '@status-im/wallet/components'
-import { useQueryClient } from '@tanstack/react-query'
-import { createFileRoute, useNavigate } from '@tanstack/react-router'
-
-import { useCreateWallet } from '../../hooks/use-create-wallet'
-
-import type { CreatePasswordFormValues } from '@status-im/wallet/components'
-import type { SubmitHandler } from 'react-hook-form'
+import { CreateWalletFlow } from '@/components/onboarding/create-wallet-flow'
export const Route = createFileRoute('/onboarding/new')({
component: RouteComponent,
})
function RouteComponent() {
- const { createWalletAsync } = useCreateWallet()
- const queryClient = useQueryClient()
- const navigate = useNavigate()
- const [isLoading, setIsLoading] = useState(false)
-
- const handleSubmit: SubmitHandler = async data => {
- setIsLoading(true)
- try {
- await createWalletAsync(data.password)
- await queryClient.invalidateQueries({ queryKey: ['session', 'status'] })
- navigate({ to: '/portfolio/assets' })
- } catch (error) {
- console.error(error)
- } finally {
- setIsLoading(false)
- }
- }
-
return (
-
-
-
- }
- aria-label="Back"
- size="32"
- />
-
-
Create password
-
- To unlock the extension and sign transactions, the password is stored
- only on your device. Status can't recover it.
-
-
-
-
-
+
)
}
diff --git a/apps/wallet/src/routes/portfolio/activity/index.tsx b/apps/wallet/src/routes/portfolio/activity/index.tsx
index 02bdb585e..e5f32e479 100644
--- a/apps/wallet/src/routes/portfolio/activity/index.tsx
+++ b/apps/wallet/src/routes/portfolio/activity/index.tsx
@@ -23,10 +23,14 @@ export const Route = createFileRoute('/portfolio/activity/')({
})
function RouteComponent() {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const { isPinExtension, handleClose } = usePinExtension()
const { pendingTransactions } = usePendingTransactions()
- const address = currentWallet?.activeAccounts[0].address
+ const address = currentAccount?.address
const toast = useToast()
diff --git a/apps/wallet/src/routes/portfolio/assets/$ticker.tsx b/apps/wallet/src/routes/portfolio/assets/$ticker.tsx
index af356a436..1cdf65825 100644
--- a/apps/wallet/src/routes/portfolio/assets/$ticker.tsx
+++ b/apps/wallet/src/routes/portfolio/assets/$ticker.tsx
@@ -21,7 +21,11 @@ export const Route = createFileRoute('/portfolio/assets/$ticker')({
})
function Component() {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const isDesktop = useMediaQuery('xl')
const params = Route.useParams()
@@ -29,7 +33,7 @@ function Component() {
const router = useRouter()
const routerState = useRouterState()
const pathname = routerState.location.pathname
- const address = currentWallet?.activeAccounts[0].address
+ const address = currentAccount?.address
const { data: assets, isLoading } = useAssets({
address,
diff --git a/apps/wallet/src/routes/portfolio/assets/index.tsx b/apps/wallet/src/routes/portfolio/assets/index.tsx
index c2a3666e1..ba39d85ce 100644
--- a/apps/wallet/src/routes/portfolio/assets/index.tsx
+++ b/apps/wallet/src/routes/portfolio/assets/index.tsx
@@ -24,12 +24,16 @@ export const Route = createFileRoute('/portfolio/assets/')({
})
function Component() {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const { isPinExtension, handleClose } = usePinExtension()
const toast = useToast()
- const address = currentWallet?.activeAccounts?.[0]?.address
+ const address = currentAccount?.address
const router = useRouter()
const { data, isLoading, isError } = useAssets({
diff --git a/apps/wallet/src/routes/portfolio/collectibles/$network/$contract/$id.tsx b/apps/wallet/src/routes/portfolio/collectibles/$network/$contract/$id.tsx
index d836c7e93..5508fbf7e 100644
--- a/apps/wallet/src/routes/portfolio/collectibles/$network/$contract/$id.tsx
+++ b/apps/wallet/src/routes/portfolio/collectibles/$network/$contract/$id.tsx
@@ -25,7 +25,11 @@ export const Route = createFileRoute(
})
function Component() {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const routerState = useRouterState()
const params = Route.useParams()
@@ -37,7 +41,7 @@ function Component() {
const search = searchParams.get('search') ?? undefined
const pathname = routerState.location.pathname
- const address = currentWallet?.activeAccounts[0].address
+ const address = currentAccount?.address
const {
data,
diff --git a/apps/wallet/src/routes/portfolio/collectibles/index.tsx b/apps/wallet/src/routes/portfolio/collectibles/index.tsx
index f5ffb8ffe..c6970172a 100644
--- a/apps/wallet/src/routes/portfolio/collectibles/index.tsx
+++ b/apps/wallet/src/routes/portfolio/collectibles/index.tsx
@@ -23,7 +23,11 @@ export const Route = createFileRoute('/portfolio/collectibles/')({
})
function Component() {
- const { currentWallet, isLoading: isWalletLoading } = useWallet()
+ const {
+ currentWallet,
+ currentAccount,
+ isLoading: isWalletLoading,
+ } = useWallet()
const { isPinExtension, handleClose } = usePinExtension()
const toast = useToast()
@@ -32,7 +36,7 @@ function Component() {
const search = searchParams.get('search') ?? undefined
const pathname = window.location.pathname
- const address = currentWallet?.activeAccounts[0].address
+ const address = currentAccount?.address
const {
data,
diff --git a/apps/wallet/src/routes/wallet-flow/import.tsx b/apps/wallet/src/routes/wallet-flow/import.tsx
new file mode 100644
index 000000000..1f6c5dc2d
--- /dev/null
+++ b/apps/wallet/src/routes/wallet-flow/import.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+import { OnboardingFlowLayout } from '@/components/onboarding/flow-layout'
+import { ImportWalletFlow } from '@/components/onboarding/import-wallet-flow'
+
+export const Route = createFileRoute('/wallet-flow/import')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+
+ )
+}
diff --git a/apps/wallet/src/routes/wallet-flow/new.tsx b/apps/wallet/src/routes/wallet-flow/new.tsx
new file mode 100644
index 000000000..bb2b53e61
--- /dev/null
+++ b/apps/wallet/src/routes/wallet-flow/new.tsx
@@ -0,0 +1,20 @@
+import { createFileRoute } from '@tanstack/react-router'
+
+import { CreateWalletFlow } from '@/components/onboarding/create-wallet-flow'
+import { OnboardingFlowLayout } from '@/components/onboarding/flow-layout'
+
+export const Route = createFileRoute('/wallet-flow/new')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ return (
+
+
+
+ )
+}
diff --git a/packages/wallet/src/components/sticky-header-container/index.tsx b/packages/wallet/src/components/sticky-header-container/index.tsx
index 3b26c896a..7b0040c60 100644
--- a/packages/wallet/src/components/sticky-header-container/index.tsx
+++ b/packages/wallet/src/components/sticky-header-container/index.tsx
@@ -30,8 +30,8 @@ const backgroundStyles = cva(
const leftSlotStyles = cva('transition-opacity duration-100 ease-in-out', {
variants: {
collapsed: {
- false: 'opacity-[0]',
- true: 'opacity-[1]',
+ false: 'pointer-events-none opacity-[0]',
+ true: 'pointer-events-auto opacity-[1]',
},
},
})
@@ -66,6 +66,14 @@ type Props = {
const SMALL_THRESHOLD = 42
const LARGE_THRESHOLD = 132
+const getFocusGuard =
+ (isVisible: boolean): React.FocusEventHandler =>
+ event => {
+ if (isVisible) return
+ if (!(event.target instanceof HTMLElement)) return
+ event.target.blur()
+ }
+
const StickyHeaderContainer = (props: Props) => {
const {
leftSlot,
@@ -126,10 +134,23 @@ const StickyHeaderContainer = (props: Props) => {
)}
>
-
{leftSlot}
-
{rightSlot}
+
+ {leftSlot}
+
+
+ {rightSlot}
+
-