Skip to content

Commit

Permalink
feat: mobile wallet keystore (#8848)
Browse files Browse the repository at this point in the history
  • Loading branch information
NeOMakinG authored Feb 18, 2025
1 parent 9e61c00 commit 49132a6
Show file tree
Hide file tree
Showing 8 changed files with 561 additions and 221 deletions.
98 changes: 98 additions & 0 deletions src/components/FileUpload/FileUpload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Box, FormControl, Icon, Input, Text as CText } from '@chakra-ui/react'
import { useColorModeValue } from '@chakra-ui/system'
import { useCallback, useState } from 'react'
import { FaFile } from 'react-icons/fa'
import { Text } from 'components/Text'

const hoverSx = { borderColor: 'blue.500' }

// TODO(gomes): use https://www.chakra-ui.com/docs/components/file-upload if/when we migrate to chakra@3
export const FileUpload = ({ onFileSelect }: { onFileSelect: (file: File) => void }) => {
const borderColor = useColorModeValue('gray.200', 'gray.600')
const [isDragging, setIsDragging] = useState(false)
const [filename, setFilename] = useState<string | null>(null)

const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}, [])

const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}, [])

const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
}, [])

const processFile = useCallback(
(file: File) => {
setFilename(file.name)
onFileSelect(file)
},
[onFileSelect],
)

const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)

const files = e.dataTransfer.files
if (files?.[0]) {
processFile(files[0])
}
},
[processFile],
)

const handleFileInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (files?.[0]) {
processFile(files[0])
}
},
[processFile],
)

return (
<FormControl>
<Input type='file' accept='.txt' onChange={handleFileInput} id='file-upload' display='none' />
<Box
as='label'
htmlFor='file-upload'
w='full'
h='32'
border='2px'
borderStyle='dashed'
borderColor={isDragging ? 'blue.500' : borderColor}
borderRadius='xl'
display='flex'
flexDirection='column'
alignItems='center'
justifyContent='center'
bg='background.surface.raised.base'
cursor='pointer'
transition='all 0.2s'
_hover={hoverSx}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Icon as={FaFile} boxSize={6} color='gray.500' mb={2} />
{filename ? (
<CText color='gray.500'>{filename}</CText>
) : (
<Text color='gray.500' translation='walletProvider.shapeShift.import.dragAndDrop' />
)}
</Box>
</FormControl>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
Box,
Button,
FormControl,
FormErrorMessage,
Input,
Text as CText,
VStack,
} from '@chakra-ui/react'
import { useCallback, useState } from 'react'
import type { FieldValues } from 'react-hook-form'
import { useForm } from 'react-hook-form'
import { useTranslate } from 'react-polyglot'
import { useHistory } from 'react-router-dom'
import { FileUpload } from 'components/FileUpload/FileUpload'
import { MobileWalletDialogRoutes } from 'components/MobileWalletDialog/types'
import { decryptFromKeystore } from 'components/MobileWalletDialog/utils'
import { DialogBackButton } from 'components/Modal/components/DialogBackButton'
import { DialogBody } from 'components/Modal/components/DialogBody'
import { DialogCloseButton } from 'components/Modal/components/DialogCloseButton'
import { DialogFooter } from 'components/Modal/components/DialogFooter'
import {
DialogHeader,
DialogHeaderLeft,
DialogHeaderRight,
} from 'components/Modal/components/DialogHeader'
import { SlideTransition } from 'components/SlideTransition'
import { Text } from 'components/Text'
import { addWallet } from 'context/WalletProvider/MobileWallet/mobileMessageHandlers'
import type { NativeWalletValues } from 'context/WalletProvider/NativeWallet/types'
import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton'
import { MixPanelEvent } from 'lib/mixpanel/types'

export const ImportKeystore = () => {
const history = useHistory()
const [keystoreFile, setKeystoreFile] = useState<string | null>(null)
const mixpanel = getMixPanel()

const translate = useTranslate()

const {
setError,
handleSubmit,
formState: { errors, isSubmitting, isValid },
register,
} = useForm<NativeWalletValues>({
mode: 'onChange',
shouldUnregister: true,
})

const onSubmit = useCallback(
async (values: FieldValues) => {
if (!keystoreFile) {
throw new Error('No keystore uploaded')
}
const parsedKeystore = JSON.parse(keystoreFile)

try {
const mnemonic = await decryptFromKeystore(parsedKeystore, values.keystorePassword)

const revocableVault = await addWallet({
mnemonic,
label: values.name.trim(),
})
history.push(MobileWalletDialogRoutes.ImportSuccess, { vault: revocableVault })
mixpanel?.track(MixPanelEvent.NativeImportKeystore)
} catch (e) {
setError('keystorePassword', {
type: 'manual',
message: translate('walletProvider.shapeShift.import.invalidKeystorePassword'),
})
}
},
[history, keystoreFile, mixpanel, setError, translate],
)

const handleFileSelect = useCallback((file: File) => {
const reader = new FileReader()
reader.onload = e => {
if (!e?.target) return
if (typeof e.target.result !== 'string') return
setKeystoreFile(e.target.result)
}
reader.readAsText(file)
}, [])

const handleGoBack = useCallback(() => {
history.goBack()
}, [history])

return (
<SlideTransition>
<DialogHeader>
<DialogHeaderLeft>
<DialogBackButton onClick={handleGoBack} />
</DialogHeaderLeft>
<DialogHeaderRight>
<DialogCloseButton />
</DialogHeaderRight>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<DialogBody>
<VStack spacing={2} mb={6} alignItems='flex-start'>
<Box>
<CText fontSize='2xl' fontWeight='bold' mb={0}>
<Text translation={'walletProvider.shapeShift.import.keystoreHeader'} />
</CText>
<CText color='text.subtle' mb={6}>
<Text translation='walletProvider.shapeShift.import.keystoreImportBody' />
</CText>
</Box>
<VStack spacing={6} width='full'>
<FormControl isInvalid={Boolean(errors.name)}>
<Input
size='lg'
variant='filled'
placeholder={translate('walletProvider.create.walletName')}
{...register('name', {
required: true,
maxLength: {
value: 64,
message: translate('modals.password.error.maxLength', {
length: 64,
}),
},
})}
/>
<FormErrorMessage>{errors.name?.message}</FormErrorMessage>
</FormControl>
<FileUpload onFileSelect={handleFileSelect} />
<FormControl isInvalid={Boolean(errors.keystorePassword)}>
<Input
type='password'
placeholder='Keystore Password'
size='lg'
variant='filled'
data-test='wallet-native-keystore-password'
{...register('keystorePassword')}
/>
<FormErrorMessage>{errors.keystorePassword?.message}</FormErrorMessage>
</FormControl>
</VStack>
</VStack>
</DialogBody>
<DialogFooter>
{keystoreFile && (
<Button
colorScheme='blue'
width='full'
size='lg'
type='submit'
isLoading={isSubmitting}
isDisabled={!isValid}
data-test='wallet-native-keystore-submit'
>
<Text translation='walletProvider.shapeShift.import.importKeystore' />
</Button>
)}
</DialogFooter>
</form>
</SlideTransition>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { MemoryRouter, Redirect, Route, Switch, useHistory } from 'react-router'
import { MobileWalletDialogRoutes } from 'components/MobileWalletDialog/types'
import { SlideTransition } from 'components/SlideTransition'

import { ImportKeystore } from './ImportKeystore'
import { ImportSeedPhrase } from './ImportSeedPhrase'
import { ImportSuccess } from './ImportSuccess'
import { ImportWallet } from './ImportWallet'

Expand All @@ -29,6 +31,12 @@ export const ImportRouter = ({ onClose, defaultRoute }: ImportRouterProps) => {
<Route path={MobileWalletDialogRoutes.ImportSuccess}>
<ImportSuccess onClose={onClose} />
</Route>
<Route path={MobileWalletDialogRoutes.ImportSeedPhrase}>
<ImportSeedPhrase />
</Route>
<Route path={MobileWalletDialogRoutes.ImportKeystore}>
<ImportKeystore />
</Route>
<Route path={MobileWalletDialogRoutes.Import}>
<ImportWallet
isDefaultRoute={defaultRoute === MobileWalletDialogRoutes.Import}
Expand Down
Loading

0 comments on commit 49132a6

Please sign in to comment.