Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e8352df
fix(wallet): disable interation w/ sticky header
JulesFiliot Apr 8, 2026
b507c4b
feat(wallet): persist selected account and normalize legacy data
JulesFiliot Apr 8, 2026
d19b43a
feat(wallet): expose and persist current account in provider
JulesFiliot Apr 8, 2026
b5a6830
refactor(wallet): use selected account in portfolio and signer flows
JulesFiliot Apr 8, 2026
a21cec5
feat(wallet): implement wallet selector UI
JulesFiliot Apr 8, 2026
ab4ad65
chore(wallet): update import and create hooks to match api
JulesFiliot Apr 8, 2026
f77f91d
feat(wallet): componentize create/import flows
JulesFiliot Apr 8, 2026
fa31e76
refactor(wallet): use components for onboarding flow
JulesFiliot Apr 8, 2026
2f096e3
feat(wallet): add standalone create and import routes
JulesFiliot Apr 8, 2026
9922f0e
chore: add changeset
JulesFiliot Apr 8, 2026
06b53bf
Merge remote-tracking branch 'origin/main' into feat/multi-wallet
JulesFiliot Apr 9, 2026
ddaa5b1
chore(wallet): hardcode account name for dapp connect
JulesFiliot Apr 9, 2026
05dadc2
Merge branch 'main' into feat/multi-wallet
JulesFiliot Apr 13, 2026
4ae49cb
chore(wallet): comment out account name tooltip
JulesFiliot Apr 15, 2026
c75b122
chore(wallet): add TODO for multi-account support
JulesFiliot Apr 15, 2026
a5e3032
chore(wallet): use wallet name instead of account name
JulesFiliot Apr 15, 2026
c5b2535
chore(wallet): rename wallet-account-selector to wallet-selector
JulesFiliot Apr 15, 2026
8614c90
fix(wallet): remove `/wallet-flow` as a route
JulesFiliot Apr 15, 2026
7bf91d5
chore(wallet): remove useless refetchQueries
JulesFiliot Apr 15, 2026
b8fd5b3
chore(wallet): edit create-password-step to factorize code
JulesFiliot Apr 15, 2026
a1cfea7
fix(wallet): rename legacy "Account 1" wallets to "Wallet N"
JulesFiliot Apr 15, 2026
d23b4f5
Merge branch 'main' into feat/multi-wallet
JulesFiliot Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/khaki-months-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@status-im/wallet': patch
'wallet': patch
---

feat(wallet): implement multi-wallets
6 changes: 3 additions & 3 deletions .changeset/lovely-mangos-protect.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
"@status-im/wallet": patch
"status.app": patch
"wallet": patch
'@status-im/wallet': patch
'status.app': patch
'wallet': patch
---

feat: connect wallet to dApp
16 changes: 16 additions & 0 deletions apps/wallet/src/components/onboarding/back-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Button } from '@status-im/components'
import { ArrowLeftIcon } from '@status-im/icons/20'

type Props = { href: string } | { onClick: () => void }

export function BackButton(props: Props) {
return (
<Button
{...props}
variant="grey"
icon={<ArrowLeftIcon color="$neutral-100" />}
aria-label="Back"
size="32"
/>
)
}
52 changes: 52 additions & 0 deletions apps/wallet/src/components/onboarding/create-password-step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
CreatePasswordForm,
type CreatePasswordFormValues,
} from '@status-im/wallet/components'

import { BackButton } from './back-button'

import type { SubmitHandler } from 'react-hook-form'

type BaseProps = {
onSubmit: SubmitHandler<CreatePasswordFormValues>
isLoading: boolean
confirmButtonLabel?: string
backButtonClassName?: string
}

type Props =
| (BaseProps & { onBack: () => void; backHref?: never })
| (BaseProps & { backHref: string; onBack?: never })

export function CreatePasswordStep({
onBack,
backHref,
onSubmit,
isLoading,
confirmButtonLabel,
backButtonClassName,
}: Props) {
return (
<div className="flex flex-col gap-4">
<div className={`flex items-center ${backButtonClassName ?? ''}`.trim()}>
{onBack ? (
<BackButton onClick={onBack} />
) : (
<BackButton href={backHref} />
)}
</div>

<h1 className="text-27 font-600">Create password</h1>
<div className="text-15 text-neutral-50">
To unlock the extension and sign transactions, the password is stored
only on your device. Status can&apos;t recover it.
</div>

<CreatePasswordForm
onSubmit={onSubmit}
loading={isLoading}
confirmButtonLabel={confirmButtonLabel}
/>
</div>
)
}
106 changes: 106 additions & 0 deletions apps/wallet/src/components/onboarding/create-wallet-flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useState } from 'react'

import { Button, Text } from '@status-im/components'
import { LoadingIcon } from '@status-im/icons/20'
import { type CreatePasswordFormValues } from '@status-im/wallet/components'

import { useCreateWallet } from '@/hooks/use-create-wallet'
import { useWalletFlowSuccess } from '@/hooks/use-wallet-flow-success'
import { usePassword } from '@/providers/password-context'

import { BackButton } from './back-button'
import { CreatePasswordStep } from './create-password-step'

import type { SubmitHandler } from 'react-hook-form'

type Props = {
backHref: string
successHref: string
requiresPasswordCreation?: boolean
}

export function CreateWalletFlow({
backHref,
successHref,
requiresPasswordCreation = true,
}: Props) {
const { createWalletAsync } = useCreateWallet()
const { requestPassword } = usePassword()
const onSuccess = useWalletFlowSuccess(successHref)
const [isLoading, setIsLoading] = useState(false)

const handleCreate = async (password?: string) => {
const wallet = await createWalletAsync(password)
await onSuccess(wallet, `Created ${wallet.name}`)
}

const handlePasswordSubmit: SubmitHandler<
CreatePasswordFormValues
> = async data => {
setIsLoading(true)
try {
await handleCreate(data.password)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}

const handleConfirmWithModal = async () => {
setIsLoading(true)
try {
const isUnlocked = await requestPassword({
title: 'Enter password',
description: 'To create a new wallet',
requireFreshPassword: true,
})
if (!isUnlocked) return
await handleCreate()
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}

if (requiresPasswordCreation) {
return (
<div className="flex h-full flex-1">
<CreatePasswordStep
backHref={backHref}
onSubmit={handlePasswordSubmit}
isLoading={isLoading}
backButtonClassName="pb-4"
confirmButtonLabel="Create wallet"
/>
</div>
)
Comment thread
JulesFiliot marked this conversation as resolved.
}

return (
<div className="flex h-full flex-1 flex-col gap-1">
<div className="flex items-center pb-4">
<BackButton href={backHref} />
</div>
<Text size={27} weight="semibold">
Create wallet
</Text>
<Text size={15} color="$neutral-50" className="mb-auto">
To create a new wallet, confirm your password in the modal.
</Text>

<Button
variant="primary"
onClick={handleConfirmWithModal}
disabled={isLoading}
>
{isLoading ? (
<LoadingIcon className="animate-spin text-white-100" />
) : (
'Create wallet'
)}
</Button>
</div>
)
}
32 changes: 32 additions & 0 deletions apps/wallet/src/components/onboarding/flow-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BlurredCircle } from '@status-im/wallet/components'

type Props = {
children: React.ReactNode
}

export function OnboardingFlowLayout({ children }: Props) {
return (
<div className="relative flex h-screen min-h-[650px] bg-neutral-5">
<BlurredCircle
color="purple"
className="absolute left-1/4 top-1/4 z-0 translate-y-[-100px]"
/>
<BlurredCircle
color="sky"
className="absolute left-2/4 top-1/4 z-0 translate-y-[-130px]"
/>
<BlurredCircle
color="yellow"
className="absolute left-1/4 top-2/4 z-0 translate-y-[-50px]"
/>
<BlurredCircle
color="orange"
className="absolute left-2/4 top-2/4 z-0 translate-y-[-80px]"
/>

<div className="relative z-10 m-auto flex min-h-[650px] w-full max-w-[440px] flex-col rounded-[24px] border border-neutral-5 bg-white-100 p-5 shadow-2">
{children}
</div>
</div>
)
}
151 changes: 151 additions & 0 deletions apps/wallet/src/components/onboarding/import-wallet-flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { useState, useTransition } from 'react'

import { Text } from '@status-im/components'
import {
type CreatePasswordFormValues,
ImportRecoveryPhraseForm,
type ImportRecoveryPhraseFormValues,
} from '@status-im/wallet/components'

import { useImportWallet } from '@/hooks/use-import-wallet'
import { useWalletFlowSuccess } from '@/hooks/use-wallet-flow-success'
import { usePassword } from '@/providers/password-context'

import { BackButton } from './back-button'
import { CreatePasswordStep } from './create-password-step'

import type { SubmitHandler } from 'react-hook-form'

type Props = {
backHref: string
successHref: string
requiresPasswordCreation?: boolean
}

type ImportFlowStep =
| { step: 'enter-mnemonic'; mnemonic: string }
| { step: 'create-password'; mnemonic: string }

export function ImportWalletFlow({
backHref,
successHref,
requiresPasswordCreation = true,
}: Props) {
const { importWalletAsync } = useImportWallet()
const { requestPassword } = usePassword()
const onSuccess = useWalletFlowSuccess(successHref)
const [isLoading, setIsLoading] = useState(false)
const [flowStep, setFlowStep] = useState<ImportFlowStep>({
step: 'enter-mnemonic',
mnemonic: '',
})

const completeImport = async (mnemonic: string, password?: string) => {
const wallet = await importWalletAsync({ mnemonic, password })
await onSuccess(wallet, `Imported ${wallet.name}`)
}

const handleMnemonicSubmit: SubmitHandler<
ImportRecoveryPhraseFormValues
> = async data => {
if (requiresPasswordCreation) {
setFlowStep({ step: 'create-password', mnemonic: data.mnemonic })
return
}

setIsLoading(true)
try {
const isUnlocked = await requestPassword({
title: 'Enter password',
description: 'To import your wallet',
requireFreshPassword: true,
})
if (!isUnlocked) return
await completeImport(data.mnemonic)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}

const handlePasswordSubmit: SubmitHandler<
CreatePasswordFormValues
> = async data => {
setIsLoading(true)
try {
await completeImport(flowStep.mnemonic, data.password)
} catch (error) {
console.error(error)
} finally {
setIsLoading(false)
}
}

if (flowStep.step === 'create-password') {
return (
<CreatePasswordStep
onBack={() =>
setFlowStep({
step: 'enter-mnemonic',
mnemonic: flowStep.mnemonic,
})
}
onSubmit={handlePasswordSubmit}
isLoading={isLoading}
confirmButtonLabel="Import Wallet"
/>
)
}

return (
<ImportMnemonicStep
backHref={backHref}
defaultMnemonic={flowStep.mnemonic}
isLoading={isLoading}
onSubmit={handleMnemonicSubmit}
/>
)
}

function ImportMnemonicStep({
backHref,
defaultMnemonic,
isLoading,
onSubmit,
}: {
backHref: string
defaultMnemonic: string
isLoading: boolean
onSubmit: SubmitHandler<ImportRecoveryPhraseFormValues>
}) {
const [isPending, startTransition] = useTransition()
const isSubmitting = isPending || isLoading

const handleSubmit: SubmitHandler<ImportRecoveryPhraseFormValues> = data => {
if (isLoading) return
startTransition(() => {
void onSubmit(data)
})
}

return (
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center pb-4">
<BackButton href={backHref} />
</div>
<Text size={27} weight="semibold">
Import via recovery phrase
</Text>
<Text size={15} color="$neutral-50" className="mb-4">
Type or paste your 12, 15, 18, 21 or 24 words Ethereum recovery phrase
</Text>

<ImportRecoveryPhraseForm
onSubmit={handleSubmit}
loading={isSubmitting}
defaultValues={{ mnemonic: defaultMnemonic }}
/>
</div>
)
}
Loading
Loading