Skip to content

Commit

Permalink
feat: mobile wallet new flow (#8689)
Browse files Browse the repository at this point in the history
* add wallet switch

* add active state

* Update MobileWalletDialog.tsx

* Update MobileWalletDialog.tsx

* move button

* add routes for wallet actions

* add delete flow

* Update SavedWallets.tsx

* fix routing of delete

* add missing translations

* Update WalletList.tsx

* comments

* check for vault outside of confirm

* Update DeleteWallet.tsx

* fix: mergefix

* fix: finish

* fix: disable same button click

* fix: try things

* fix: review feedbacks

* fix: a few more bugs

---------

Co-authored-by: reallybeard <[email protected]>
  • Loading branch information
NeOMakinG and reallybeard authored Feb 4, 2025
1 parent 8489655 commit 7a87bf4
Show file tree
Hide file tree
Showing 21 changed files with 1,116 additions and 349 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@
"stream-http": "^3.2.0",
"styled-components": "^6.0.7",
"uuid": "^9.0.0",
"vaul": "^0.9.0",
"vaul": "^1.1.2",
"viem": "^2.10.9",
"wagmi": "^2.9.2",
"web-vitals": "^2.1.4"
Expand Down
14 changes: 12 additions & 2 deletions src/assets/translations/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@
"and": "and",
"balance": "Balance",
"next": "Next",
"edit": "Edit",
"error": "Error",
"show": "Show",
"account": "Account",
"address": "Address",
"noThanks": "No Thanks",
"done": "Done",
"cancel": "Cancel",
"close": "Close",
Expand Down Expand Up @@ -1238,6 +1240,11 @@
"menuItem": "Back up my wallet",
"enterPassword": "Enter your password to continue",
"testTitle": "Verify Your Secret Recovery Phrase",
"confirm": {
"title": "Backup wallet",
"body": "Before you forget your wallet, would you like to back up your secret recovery phrase?",
"backupNow": "Backup Now"
},
"info": {
"title": "Your Secret Recovery Phrase",
"description": "Write these 12 words down and store them securely offline. This 12 word phrase is used to recover your wallet private keys.",
Expand Down Expand Up @@ -1764,12 +1771,15 @@
"error": {
"delete": "Unable to delete your wallet, sorry.",
"noWallet": "You have no saved wallets",
"pair": "Unable to pair your wallet"
"pair": "Unable to pair your wallet",
"fetchingWallets": "An error occurred while fetching wallets."
},
"header": "Saved Wallets",
"body": "Loading your saved wallet... Enter your password when prompted.",
"button": "Continue",
"confirmForget": "Are you sure you want to forget %{wallet}?"
"forgetWallet": "Forget Wallet",
"confirmForget": "Are you sure you want to forget %{wallet}?",
"confirmForgetBody": "You will not be able to access your wallet if you have not backed it up."
},
"import": {
"header": "Import your wallet",
Expand Down
40 changes: 40 additions & 0 deletions src/components/MobileWalletDialog/MobileWalletDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AnimatePresence } from 'framer-motion'
import { MemoryRouter, Redirect, Route, Switch } from 'react-router'
import { Dialog } from 'components/Modal/components/Dialog'

import { DeleteWallet } from './routes/DeleteWallet/DeleteWallet'
import { RenameWallet } from './routes/RenameWallet'
import { SavedWallets } from './routes/SavedWallets'
import { MobileWalletDialogRoutes } from './types'

type MobileWalletDialogProps = {
isOpen: boolean
onClose: () => void
}

export const MobileWalletDialog: React.FC<MobileWalletDialogProps> = ({ isOpen, onClose }) => {
return (
<Dialog isOpen={isOpen} onClose={onClose} height='auto' isDisablingPropagation={false}>
<MemoryRouter>
<Route>
{({ location }) => (
<AnimatePresence mode='wait' initial={false}>
<Switch key={location.key} location={location}>
<Route path={MobileWalletDialogRoutes.Saved}>
<SavedWallets onClose={onClose} />
</Route>
<Route path={MobileWalletDialogRoutes.Rename}>
<RenameWallet />
</Route>
<Route path={MobileWalletDialogRoutes.Delete}>
<DeleteWallet />
</Route>
<Redirect exact from='/' to={MobileWalletDialogRoutes.Saved} />
</Switch>
</AnimatePresence>
)}
</Route>
</MemoryRouter>
</Dialog>
)
}
60 changes: 60 additions & 0 deletions src/components/MobileWalletDialog/routes/DeleteWallet/Backup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Button, Heading, Stack } from '@chakra-ui/react'
import React, { useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import { useHistory } from 'react-router'
import { MobileWalletDialogRoutes } from 'components/MobileWalletDialog/types'
import { DialogBackButton } from 'components/Modal/components/DialogBackButton'
import { DialogBody } from 'components/Modal/components/DialogBody'
import { DialogFooter } from 'components/Modal/components/DialogFooter'
import { DialogHeader, DialogHeaderLeft } from 'components/Modal/components/DialogHeader'
import { SlideTransition } from 'components/SlideTransition'
import { RawText } from 'components/Text'
import { useModal } from 'hooks/useModal/useModal'

// TODO: This is placeholder content follow up PR will implement this correctly

type BackupProps = {
onBack: () => void
}

export const Backup: React.FC<BackupProps> = ({ onBack }) => {
const translate = useTranslate()
const history = useHistory()
const backupModal = useModal('backupNativePassphrase')

const handleContinue = useCallback(() => {
history.push(MobileWalletDialogRoutes.ConfirmDelete)
}, [history])

const handleBackup = useCallback(() => {
backupModal.open({})
}, [backupModal])

return (
<SlideTransition>
<DialogHeader>
<DialogHeaderLeft>
<DialogBackButton onClick={onBack} />
</DialogHeaderLeft>
</DialogHeader>
<DialogBody pb={8}>
<Stack>
<Heading size='md' textAlign='center' maxWidth='250px' mx='auto'>
{translate('modals.shapeShift.backupPassphrase.confirm.title')}
</Heading>
<RawText textAlign='center' maxWidth='300px' mx='auto' mb={6} color='text.subtle'>
{translate('modals.shapeShift.backupPassphrase.confirm.body')}
</RawText>
</Stack>
</DialogBody>
<DialogFooter flexDir='column' gap={2}>
<Button colorScheme='blue' size='lg' width='full' onClick={handleBackup}>
{translate('modals.shapeShift.backupPassphrase.confirm.backupNow')}
</Button>
<Button colorScheme='gray' size='lg' width='full' onClick={handleContinue}>
{translate('common.noThanks')}
</Button>
</DialogFooter>
</SlideTransition>
)
}
72 changes: 72 additions & 0 deletions src/components/MobileWalletDialog/routes/DeleteWallet/Confirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { WarningIcon } from '@chakra-ui/icons'
import { Alert, AlertDescription, Button, Heading, Stack, Text } from '@chakra-ui/react'
import { useCallback, useState } from 'react'
import { useTranslate } from 'react-polyglot'
import { DialogBackButton } from 'components/Modal/components/DialogBackButton'
import { DialogBody } from 'components/Modal/components/DialogBody'
import { DialogFooter } from 'components/Modal/components/DialogFooter'
import { DialogHeader, DialogHeaderLeft } from 'components/Modal/components/DialogHeader'
import { SlideTransition } from 'components/SlideTransition'
import { deleteWallet } from 'context/WalletProvider/MobileWallet/mobileMessageHandlers'
import type { RevocableWallet } from 'context/WalletProvider/MobileWallet/RevocableWallet'
import { useWallet } from 'hooks/useWallet/useWallet'
import { WalletCard } from 'pages/ConnectWallet/components/WalletCard'

type ConfirmDeleteProps = {
vault: RevocableWallet
onBack: () => void
}

export const ConfirmDelete: React.FC<ConfirmDeleteProps> = ({ vault, onBack }) => {
const [error, setError] = useState<string | null>(null)
const translate = useTranslate()
const { disconnect, state } = useWallet()

const handleDelete = useCallback(async () => {
if (vault?.id) {
try {
await deleteWallet(vault.id)

if (state.walletInfo?.deviceId === vault.id) {
disconnect()
}
onBack()
} catch (e) {
console.log(e)
setError('walletProvider.shapeShift.load.error.delete')
}
}
}, [onBack, vault?.id, disconnect, state.walletInfo?.deviceId])

return (
<SlideTransition>
<DialogHeader>
<DialogHeaderLeft>
<DialogBackButton onClick={onBack} />
</DialogHeaderLeft>
</DialogHeader>
<DialogBody>
<Stack mb={4}>
<WarningIcon color='text.error' boxSize='48px' mx='auto' />
<Heading size='md' textAlign='center' maxWidth='250px' mx='auto'>
{translate('walletProvider.shapeShift.load.confirmForget', { wallet: vault?.label })}
</Heading>
<Text textAlign='center' maxWidth='300px' mx='auto' mb={6} color='text.subtle'>
{translate('walletProvider.shapeShift.load.confirmForgetBody')}
</Text>
<WalletCard wallet={vault} id={vault.id} />
</Stack>
</DialogBody>
<DialogFooter flexDir='column' gap={2}>
{error && (
<Alert status='error'>
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button size='lg' colorScheme='red' width='full' onClick={handleDelete}>
{translate('walletProvider.shapeShift.load.forgetWallet')}
</Button>
</DialogFooter>
</SlideTransition>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button, Heading } from '@chakra-ui/react'
import { AnimatePresence } from 'framer-motion'
import { useCallback } from 'react'
import { useTranslate } from 'react-polyglot'
import { MemoryRouter, Redirect, Route, Switch, useHistory, useLocation } from 'react-router'
import { MobileWalletDialogRoutes } from 'components/MobileWalletDialog/types'
import { DialogBody } from 'components/Modal/components/DialogBody'
import { SlideTransition } from 'components/SlideTransition'
import type { MobileLocationState } from 'context/WalletProvider/MobileWallet/types'

import { Backup } from './Backup'
import { ConfirmDelete } from './Confirm'

export const DeleteWallet = () => {
const {
state: { vault },
} = useLocation<MobileLocationState>()
const history = useHistory()
const translate = useTranslate()

const handleBack = useCallback(() => {
history.push(MobileWalletDialogRoutes.Saved)
}, [history])

if (!vault)
return (
<SlideTransition>
<DialogBody>
<Heading size='md' textAlign='center' maxWidth='250px' mx='auto'>
{translate('common.somethingWentWrong')}
</Heading>
<Button onClick={handleBack} mx='auto'>
{translate('common.goBack')}
</Button>
</DialogBody>
</SlideTransition>
)

return (
<SlideTransition>
<MemoryRouter>
<Route>
{({ location }) => (
<AnimatePresence mode='wait' initial={false}>
<Switch key={location.key} location={location}>
<Route path={MobileWalletDialogRoutes.Backup}>
<Backup onBack={handleBack} />
</Route>
<Route path={MobileWalletDialogRoutes.ConfirmDelete}>
<ConfirmDelete vault={vault} onBack={handleBack} />
</Route>
{/* TODO: This will change to backup in a follow up PR */}
<Redirect from='/' to={MobileWalletDialogRoutes.ConfirmDelete} />
</Switch>
</AnimatePresence>
)}
</Route>
</MemoryRouter>
</SlideTransition>
)
}
99 changes: 99 additions & 0 deletions src/components/MobileWalletDialog/routes/RenameWallet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Button, FormControl, FormErrorMessage, Input } from '@chakra-ui/react'
import { useCallback } from 'react'
import { useForm } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { useHistory, useLocation } from 'react-router'
import { DialogBackButton } from 'components/Modal/components/DialogBackButton'
import { DialogBody } from 'components/Modal/components/DialogBody'
import { DialogFooter } from 'components/Modal/components/DialogFooter'
import {
DialogHeader,
DialogHeaderLeft,
DialogHeaderMiddle,
} from 'components/Modal/components/DialogHeader'
import { DialogTitle } from 'components/Modal/components/DialogTitle'
import { SlideTransition } from 'components/SlideTransition'
import { Text } from 'components/Text'
import { updateWallet } from 'context/WalletProvider/MobileWallet/mobileMessageHandlers'
import type { MobileLocationState } from 'context/WalletProvider/MobileWallet/types'

import { MobileWalletDialogRoutes } from '../types'

type FormValues = {
label: string
}

export const RenameWallet = () => {
const location = useLocation<MobileLocationState>()
const history = useHistory()
const translate = useTranslate()
const {
handleSubmit,
register,
formState: { errors, isSubmitting, isValid },
} = useForm<FormValues>({
mode: 'onChange',
defaultValues: { label: location.state.vault?.label },
})

const onSubmit = useCallback(
async (values: FormValues) => {
if (!location.state.vault?.id) return
try {
await updateWallet(location.state.vault.id, { label: values.label })
history.goBack()
} catch (e) {
console.log(e)
}
},
[history, location.state.vault?.id],
)

const handleBack = useCallback(() => history.push(MobileWalletDialogRoutes.Saved), [history])

return (
<SlideTransition>
<DialogHeader>
<DialogHeaderLeft>
<DialogBackButton onClick={handleBack} />
</DialogHeaderLeft>
<DialogHeaderMiddle>
<DialogTitle>{translate('walletProvider.shapeShift.rename.header')}</DialogTitle>
</DialogHeaderMiddle>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogBody>
<Text mb={6} color='text.subtle' translation={'walletProvider.shapeShift.rename.body'} />
<FormControl mb={6} isInvalid={!!errors.label}>
<Input
size='lg'
variant='filled'
id='name'
placeholder={translate('walletProvider.shapeShift.rename.walletName')}
{...register('label', {
required: true,
maxLength: {
value: 64,
message: translate('modals.shapeShift.password.error.maxLength', { length: 64 }),
},
})}
/>
<FormErrorMessage>{errors.label && errors.label.message}</FormErrorMessage>
</FormControl>
</DialogBody>
<DialogFooter pt={4}>
<Button
colorScheme='blue'
size='lg'
width='full'
isLoading={isSubmitting}
type='submit'
isDisabled={!isValid}
>
<Text translation={'walletProvider.shapeShift.rename.button'} />
</Button>
</DialogFooter>
</form>
</SlideTransition>
)
}
Loading

0 comments on commit 7a87bf4

Please sign in to comment.