Skip to content

Commit

Permalink
Merge branch 'develop' into feat_by_asset_2
Browse files Browse the repository at this point in the history
  • Loading branch information
gomesalexandre authored Feb 19, 2025
2 parents ec2b457 + bffce37 commit d54ece4
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const makeOrdinalSuffix = (n: number) => {
return ['st', 'nd', 'rd'][((((n + 90) % 100) - 10) % 10) - 1] || 'th'
}

const TEST_COUNT_REQUIRED = 4
const TEST_COUNT_REQUIRED = 3

export const CreateBackupConfirm = () => {
const history = useHistory()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,9 @@ export const TradeQuote: FC<TradeQuoteProps> = memo(
)

const handleQuoteSelection = useCallback(() => {
if (!isActive) {
dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData))
onBack && onBack()
} else if (!isBest) {
// don't allow un-selecting of best quote as it gets re-selected in this case
dispatch(tradeQuoteSlice.actions.setActiveQuote(undefined))
}
}, [dispatch, isActive, isBest, onBack, quoteData])
dispatch(tradeQuoteSlice.actions.setActiveQuote(quoteData))
onBack && onBack()
}, [dispatch, onBack, quoteData])

const feeAsset = useAppSelector(state =>
selectFeeAssetByChainId(state, sellAsset.chainId ?? ''),
Expand Down
210 changes: 146 additions & 64 deletions src/context/WalletProvider/NativeWallet/components/NativeTestPhrase.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
import { Box, Button, Checkbox, Divider, ModalBody, ModalHeader, Tag, Wrap } from '@chakra-ui/react'
import {
Box,
Button,
Checkbox,
Divider,
Flex,
Icon,
ModalBody,
ModalHeader,
Text as CText,
useColorModeValue,
VStack,
} from '@chakra-ui/react'
import { Default } from '@shapeshiftoss/hdwallet-native/dist/crypto/isolation/engines'
import * as bip39 from 'bip39'
import range from 'lodash/range'
import shuffle from 'lodash/shuffle'
import slice from 'lodash/slice'
import uniq from 'lodash/uniq'
import { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FaCheck } from 'react-icons/fa'
import { useTranslate } from 'react-polyglot'
import { RawText, Text } from 'components/Text'
import { Text } from 'components/Text'

import type { NativeSetupProps } from '../types'

Expand All @@ -28,13 +41,33 @@ type TestState = {

export const NativeTestPhrase = ({ history, location }: NativeSetupProps) => {
const translate = useTranslate()
const borderColor = useColorModeValue('gray.300', 'whiteAlpha.200')
const dottedTitleBackground = useColorModeValue('#f7fafc', '#2e3236')
const [testState, setTestState] = useState<TestState | null>(null)
const [hasAlreadySaved, setHasAlreadySaved] = useState(false)
const [invalidTries, setInvalidTries] = useState<number[]>([])
const [testCount, setTestCount] = useState<number>(0)
const testCount = useRef(0)
const [revoker] = useState(new (Revocable(class {}))())
const [shuffledNumbers] = useState(slice(shuffle(range(12)), 0, TEST_COUNT_REQUIRED))
const [, setError] = useState<string | null>(null)
const isInitiallyShuffled = useRef(false)

const backgroundDottedSx = useMemo(
() => ({
content: '""',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderWidth: 1,
borderStyle: 'dashed',
borderColor,
borderRadius: 'xl',
mask: 'linear-gradient(to bottom, black 20%, transparent 100%)',
WebkitMask: 'linear-gradient(to bottom, black 20%, transparent 100%)',
}),
[borderColor],
)

const onCheck = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
// Check the captcha in case the captcha has been validated
Expand All @@ -45,13 +78,13 @@ export const NativeTestPhrase = ({ history, location }: NativeSetupProps) => {
const { vault, isLegacyWallet } = location.state

const shuffleMnemonic = useCallback(async () => {
if (testCount >= TEST_COUNT_REQUIRED) return
if (testCount.current >= TEST_COUNT_REQUIRED) return
try {
const mnemonic = await vault.unwrap().get('#mnemonic')
const words = mnemonic.split(' ')
let randomWords = uniq(bip39.generateMnemonic(256).split(' ')) as string[]

const targetWordIndex = shuffledNumbers[testCount]
const targetWordIndex = shuffledNumbers[testCount.current]
const targetWord = words[targetWordIndex]
randomWords = randomWords.filter(x => x !== targetWord).slice(0, 14)
randomWords.push(targetWord)
Expand All @@ -76,27 +109,29 @@ export const NativeTestPhrase = ({ history, location }: NativeSetupProps) => {
}, [setTestState, shuffledNumbers, vault, revoker, testCount])

useEffect(() => {
shuffleMnemonic().catch(() => setError('walletProvider.shapeShift.create.error'))
if (!isInitiallyShuffled.current) {
shuffleMnemonic()
isInitiallyShuffled.current = true
}
}, [shuffleMnemonic])

useEffect(() => {
// If we've passed the required number of tests, then we can proceed
if (testCount >= TEST_COUNT_REQUIRED) {
vault.seal()
history.replace('/native/password', { vault })
return () => {
// Make sure the component is completely unmounted before we revoke the mnemonic
setTimeout(() => revoker.revoke(), 250)
}
}
}, [testCount, history, revoker, vault])
const handleBackupComplete = useCallback(() => {
vault.seal()
history.replace('/native/password', { vault })
setTimeout(() => revoker.revoke(), 250)
}, [history, revoker, vault])

const handleClick = (index: number) => {
if (index === testState?.correctAnswerIndex) {
setInvalidTries([])
setTestCount(testCount + 1)
testCount.current++

if (testCount.current >= TEST_COUNT_REQUIRED) {
handleBackupComplete()
} else {
shuffleMnemonic()
}
} else {
setInvalidTries([...invalidTries, index])
shuffleMnemonic()
}
}

Expand All @@ -108,50 +143,97 @@ export const NativeTestPhrase = ({ history, location }: NativeSetupProps) => {
return !testState ? null : (
<>
<ModalHeader>
<Text translation={'walletProvider.shapeShift.testPhrase.header'} />
<Text translation={'modals.shapeShift.backupPassphrase.title'} />
</ModalHeader>
<ModalBody>
<RawText>
<Text
as='span'
color='text.subtle'
translation={'walletProvider.shapeShift.testPhrase.body'}
/>{' '}
<Tag colorScheme='green'>
{translate(
`walletProvider.shapeShift.testPhrase.${testState.targetWordIndex + 1}${ordinalSuffix(
testState.targetWordIndex + 1,
)}`,
)}
<Text as='span' ml={1} translation={'walletProvider.shapeShift.testPhrase.body2'} />
</Tag>{' '}
<Text
as='span'
color='text.subtle'
translation={'walletProvider.shapeShift.testPhrase.body3'}
/>
</RawText>
<Wrap mt={12} mb={6}>
{testState &&
testState.randomWords.map((word: string, index: number) =>
revocable(
<Button
key={index}
flexGrow={4}
flexBasis='auto'
variant='ghost-filled'
colorScheme={invalidTries.includes(index) ? 'gray' : 'blue'}
isDisabled={invalidTries.includes(index)}
// we need to pass an arg here, so we need an anonymous function wrapper
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(index)}
>
{word}
</Button>,
revoker.addRevoker.bind(revoker),
),
)}
</Wrap>
<Text
color='text.subtle'
translation={'modals.shapeShift.backupPassphrase.description'}
mb={12}
/>
<VStack spacing={6} alignItems='stretch'>
<Box borderRadius='xl' p={6} position='relative' pb={20}>
<CText
textAlign='center'
position='absolute'
pointerEvents='none'
zIndex='1'
top='0'
left='50%'
transform='translateX(-50%) translateY(-50%)'
px={2}
width='max-content'
color='text.subtle'
bg={dottedTitleBackground}
>
<Text as='span' translation={'walletProvider.shapeShift.testPhrase.body'} />{' '}
<Box as='span' color='blue.500' fontWeight='bold'>
{translate(
`walletProvider.shapeShift.testPhrase.${
testState.targetWordIndex + 1
}${ordinalSuffix(testState.targetWordIndex + 1)}`,
)}
</Box>
<Text as='span' ml={1} translation={'walletProvider.shapeShift.testPhrase.body2'} />
</CText>

<Box
width='100%'
height='100%'
position='absolute'
borderRadius='xl'
pointerEvents='none'
left='0'
top='0'
_before={backgroundDottedSx}
/>

<Flex wrap='wrap' justify='center' gap={2}>
{testState.randomWords.map((word: string, index: number) =>
revocable(
<Button
key={`${word}-${index}`}
variant='solid'
size='md'
colorScheme='gray'
// eslint-disable-next-line react-memo/require-usememo
onClick={() => handleClick(index)}
px={4}
py={2}
height='auto'
borderRadius='lg'
>
{word}
</Button>,
revoker.addRevoker.bind(revoker),
),
)}
</Flex>
</Box>
</VStack>

<Flex justifyContent='center' mt={6}>
<Flex gap={2} justify='center'>
{Array.from({ length: TEST_COUNT_REQUIRED }).map((_, index) => (
<Box
key={index}
w='16px'
h='16px'
borderRadius='full'
bg={index < testCount.current ? 'blue.500' : 'transparent'}
borderWidth={1}
borderStyle='dashed'
borderColor={borderColor}
display='flex'
alignItems='center'
justifyContent='center'
>
{index < testCount.current && <Icon as={FaCheck} boxSize='8px' color='white' />}
</Box>
))}
</Flex>
</Flex>

{isLegacyWallet && (
<Box>
<Box position='relative' mb={8} mt={10}>
Expand Down
2 changes: 1 addition & 1 deletion src/state/slices/opportunitiesSlice/selectors/combined.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type {
OpportunityId,
StakingEarnOpportunityType,
} from '../types'
import { DefiProvider, DefiType } from '../types'
import { DefiType } from '../types'
import { getOpportunityAccessor, getUnderlyingAssetIdsBalances } from '../utils'
import { selectAssets } from './../../assetsSlice/selectors'
import { selectMarketDataUserCurrency } from './../../marketDataSlice/selectors'
Expand Down

0 comments on commit d54ece4

Please sign in to comment.