From d6b5780108aef084b9b8efe7da4e4fdd9a354b21 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 10:43:18 +0000 Subject: [PATCH 1/8] feat(onboarding): implementing new search account screen --- apps/mobile/assets/icons/phone.svg | 4 ++ .../src/components/core/PWIcon/PWIcon.tsx | 2 + apps/mobile/src/i18n/locales/en.json | 3 + .../src/modules/onboarding/routes/index.tsx | 12 ++++ .../src/modules/onboarding/routes/types.ts | 1 + .../useImportAccountScreen.ts | 10 ++-- .../SearchAccountsScreen.tsx | 51 +++++++++++++++++ .../__tests__/SearchAccountsScreen.spec.tsx | 32 +++++++++++ .../screens/SearchAccountsScreen/index.ts | 13 +++++ .../screens/SearchAccountsScreen/styles.ts | 55 +++++++++++++++++++ .../useSearchAccountsScreen.ts | 25 +++++++++ 11 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 apps/mobile/assets/icons/phone.svg create mode 100644 apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx create mode 100644 apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx create mode 100644 apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/index.ts create mode 100644 apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts create mode 100644 apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts diff --git a/apps/mobile/assets/icons/phone.svg b/apps/mobile/assets/icons/phone.svg new file mode 100644 index 00000000..0e1d1589 --- /dev/null +++ b/apps/mobile/assets/icons/phone.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/components/core/PWIcon/PWIcon.tsx b/apps/mobile/src/components/core/PWIcon/PWIcon.tsx index ce88b80b..3afea31b 100644 --- a/apps/mobile/src/components/core/PWIcon/PWIcon.tsx +++ b/apps/mobile/src/components/core/PWIcon/PWIcon.tsx @@ -63,6 +63,7 @@ import PlusIcon from '@assets/icons/plus.svg' import QuestionMarkIcon from '@assets/icons/question-mark.svg' import RekeyIcon from '@assets/icons/rekey.svg' import ReloadIcon from '@assets/icons/reload.svg' +import PhoneIcon from '@assets/icons/phone.svg' import ShareIcon from '@assets/icons/share.svg' import ShieldCheckIcon from '@assets/icons/shield-check.svg' import SlidersIcon from '@assets/icons/sliders.svg' @@ -152,6 +153,7 @@ const ICON_LIBRARY = { 'question-mark': QuestionMarkIcon, rekey: RekeyIcon, reload: ReloadIcon, + phone: PhoneIcon, share: ShareIcon, 'shield-check': ShieldCheckIcon, sliders: SlidersIcon, diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index cd5b3142..1468fcef 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -300,6 +300,9 @@ "failed_title": "Import failed", "failed_body": "There was an error trying to import your wallet" }, + "searching_accounts": { + "title": "Searching your accounts" + }, "create_account": { "processing": "Setting up your wallet...", "error_title": "Failed to create account", diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 7d1cc066..2ff8920e 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -26,6 +26,8 @@ import { screenListeners } from '@routes/listeners' import { fullScreenLayout, safeAreaLayout } from '@layouts/index' import type React from 'react' +import { SearchAccountsScreen } from '@modules/onboarding/screens/SearchAccountsScreen' + // Wrap screens with AccountErrorBoundary to catch account-related errors const withAccountErrorBoundary =

( WrappedComponent: React.ComponentType

, @@ -48,6 +50,8 @@ const ImportAccountScreenWithErrorBoundary = withAccountErrorBoundary(ImportAccountScreen) const ImportInfoScreenWithErrorBoundary = withAccountErrorBoundary(ImportInfoScreen) +const SearchAccountsScreenWithErrorBoundary = + withAccountErrorBoundary(SearchAccountsScreen) import { OnboardingStackParamList } from './types' export type { OnboardingStackParamList } from './types' @@ -97,6 +101,14 @@ export const OnboardingStackNavigator = () => { layout={safeAreaLayout} component={ImportAccountScreenWithErrorBoundary} /> + ) } diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index 7776b333..f0888128 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -28,4 +28,5 @@ export type OnboardingStackParamList = { ImportAccount: { accountType: ImportAccountType } + SearchAccounts: undefined } diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 068f230a..9a5f6005 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -101,10 +101,8 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { [mnemonicLength], ) - const goToHome = useCallback(() => { - navigation.replace('TabBar', { - screen: 'Home', - }) + const goToSearchAccounts = useCallback(() => { + navigation.replace('SearchAccounts') }, [navigation]) const handleImportAccount = useCallback(() => { @@ -115,7 +113,7 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { mnemonic: words.join(' '), type: accountType, }) - goToHome() + goToSearchAccounts() } catch { showToast({ title: t('onboarding.import_account.failed_title'), @@ -126,7 +124,7 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { setProcessing(false) } }, 0) - }, [importAccount, words, goToHome, showToast, t]) + }, [importAccount, words, goToSearchAccounts, showToast, t]) return { words, diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx new file mode 100644 index 00000000..31e88b0f --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx @@ -0,0 +1,51 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { PWText, PWView } from '@components/core' +import { RoundButton } from '@components/RoundButton/RoundButton' +import { useStyles } from './styles' +import { useSearchAccountsScreen } from './useSearchAccountsScreen' + +export const SearchAccountsScreen = () => { + const styles = useStyles() + const { t } = useSearchAccountsScreen() + + return ( + + + + + + + + + + + + + + + + {t('onboarding.searching_accounts.title')} + + + ) +} diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx new file mode 100644 index 00000000..2813a5e0 --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx @@ -0,0 +1,32 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { render, screen } from '@test-utils/render' +import { SearchAccountsScreen } from '../SearchAccountsScreen' + +describe('SearchAccountsScreen', () => { + it('renders searching accounts title', () => { + render() + expect( + screen.getByText('onboarding.searching_accounts.title'), + ).toBeTruthy() + }) + + it('renders globe and phone icons', () => { + render() + // Since RoundButtons are disabled, they might be harder to find by role, + // but we can check if they are rendered. + // PWIcon rendered globe and phone icons + expect(screen.getByTestId('icon-globe')).toBeTruthy() + expect(screen.getByTestId('icon-phone')).toBeTruthy() + }) +}) diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/index.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/index.ts new file mode 100644 index 00000000..89d21bab --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/index.ts @@ -0,0 +1,13 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +export { SearchAccountsScreen } from './SearchAccountsScreen' diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts new file mode 100644 index 00000000..df2926ca --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts @@ -0,0 +1,55 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { makeStyles } from '@rneui/themed' + +export const useStyles = makeStyles(theme => ({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + justifyContent: 'center', + alignItems: 'center', + padding: theme.spacing.xl, + }, + topRow: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.spacing.xl, + }, + dotsContainer: { + flexDirection: 'row', + alignItems: 'center', + marginHorizontal: theme.spacing.xl, + gap: theme.spacing.sm, + }, + dot: { + width: 8, + height: 8, + borderRadius: 4, + }, + dot1: { + backgroundColor: theme.colors.grey2, // Gray 200 + }, + dot2: { + backgroundColor: theme.colors.grey4, // Gray 400 + }, + dot3: { + backgroundColor: theme.colors.grey5, // Gray 600 + }, + dot4: { + backgroundColor: theme.colors.black, // Gray 800 + }, + title: { + textAlign: 'center', + marginTop: theme.spacing.md, + }, +})) diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts new file mode 100644 index 00000000..b4b8fb75 --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts @@ -0,0 +1,25 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { useLanguage } from '@hooks/useLanguage' + +export type UseSearchAccountsScreenResult = { + t: (key: string) => string +} + +export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { + const { t } = useLanguage() + + return { + t, + } +} From d2573563c07b2dfb3e8100eebedb4097e2ee713b Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:02:10 +0000 Subject: [PATCH 2/8] feat(onboarding): implement support for pasting mnemonics --- apps/mobile/src/i18n/locales/en.json | 6 +- .../__tests__/useImportAccountScreen.spec.ts | 149 ++++++++++++++++++ .../useImportAccountScreen.ts | 67 ++++++-- 3 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/__tests__/useImportAccountScreen.spec.ts diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 1468fcef..33d8c20d 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -298,7 +298,11 @@ "importing": "Importing wallet...", "button": "Import Wallet", "failed_title": "Import failed", - "failed_body": "There was an error trying to import your wallet" + "failed_body": "There was an error trying to import your wallet", + "invalid_mnemonic_title": "Invalid Passphrase", + "invalid_mnemonic_body": "The passphrase you pasted contains too many words.", + "insufficient_slots_title": "Invalid Passphrase", + "insufficient_slots_body": "The passphrase you pasted does not fit in the remaining slots." }, "searching_accounts": { "title": "Searching your accounts" diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/__tests__/useImportAccountScreen.spec.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/__tests__/useImportAccountScreen.spec.ts new file mode 100644 index 00000000..86255233 --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/__tests__/useImportAccountScreen.spec.ts @@ -0,0 +1,149 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { renderHook, act } from '@testing-library/react' +import { useImportAccountScreen } from '../useImportAccountScreen' +import { vi, describe, it, expect, beforeEach } from 'vitest' + +const mockShowToast = vi.fn() +const mockReplace = vi.fn() +const mockGoBack = vi.fn() + +vi.mock('react-native', () => ({ + Keyboard: { + addListener: vi.fn(() => ({ + remove: vi.fn(), + })), + }, + Platform: { + OS: 'ios', + }, +})) + +vi.mock('@react-navigation/native', () => ({ + useRoute: vi.fn(() => ({ + params: { accountType: 'hdWallet' }, + })), +})) + +vi.mock('@hooks/useAppNavigation', () => ({ + useAppNavigation: vi.fn(() => ({ + replace: mockReplace, + goBack: mockGoBack, + })), +})) + +vi.mock('@perawallet/wallet-core-accounts', () => ({ + useImportAccount: vi.fn(), +})) + +vi.mock('@hooks/useToast', () => ({ + useToast: vi.fn(() => ({ + showToast: mockShowToast, + })), +})) + +vi.mock('@hooks/useLanguage', () => ({ + useLanguage: vi.fn(() => ({ + t: (key: string) => key, + })), +})) + +describe('useImportAccountScreen', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('initializes with empty words', () => { + const { result } = renderHook(() => useImportAccountScreen()) + expect(result.current.words).toHaveLength(24) + expect(result.current.words.every(w => w === '')).toBe(true) + }) + + it('updates a single word at a specific index', () => { + const { result } = renderHook(() => useImportAccountScreen()) + + act(() => { + result.current.updateWord('apple', 5) + }) + + expect(result.current.words[5]).toBe('apple') + }) + + it('fills all slots when a full mnemonic is pasted into any slot', () => { + const { result } = renderHook(() => useImportAccountScreen()) + const mnemonic = new Array(24).fill('word').join(' ') + + act(() => { + result.current.updateWord(mnemonic, 5) + }) + + expect(result.current.words.every(w => w === 'word')).toBe(true) + }) + + it('fills sequential slots when a partial mnemonic is pasted into any slot and fits', () => { + const { result } = renderHook(() => useImportAccountScreen()) + const mnemonic = 'word1 word2 word3' + + act(() => { + result.current.updateWord(mnemonic, 20) + }) + + expect(result.current.words[20]).toBe('word1') + expect(result.current.words[21]).toBe('word2') + expect(result.current.words[22]).toBe('word3') + expect(result.current.words[23]).toBe('') + }) + + it('shows insufficient slots toast if partial mnemonic does not fit in remaining slots', () => { + const { result } = renderHook(() => useImportAccountScreen()) + const mnemonic = 'word1 word2 word3' + + act(() => { + result.current.updateWord(mnemonic, 22) + }) + + expect(mockShowToast).toHaveBeenCalledWith({ + title: 'onboarding.import_account.insufficient_slots_title', + body: 'onboarding.import_account.insufficient_slots_body', + type: 'error', + }) + expect(result.current.words[22]).toBe('') + }) + + it('shows a toast and does not update words if too many words are pasted', () => { + const { result } = renderHook(() => useImportAccountScreen()) + const mnemonic = new Array(25).fill('word').join(' ') + + act(() => { + result.current.updateWord(mnemonic, 0) + }) + + expect(mockShowToast).toHaveBeenCalledWith({ + title: 'onboarding.import_account.invalid_mnemonic_title', + body: 'onboarding.import_account.invalid_mnemonic_body', + type: 'error', + }) + expect(result.current.words[0]).toBe('') + }) + + it('treats spaces at the end of a single word as a single word update', () => { + const { result } = renderHook(() => useImportAccountScreen()) + + act(() => { + result.current.updateWord('apple ', 0) + }) + + expect(result.current.words[0]).toBe('apple') + expect(result.current.words[1]).toBe('') + }) +}) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index 9a5f6005..df1d0888 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -86,19 +86,64 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { const updateWord = useCallback( (word: string, index: number) => { - const splitWords = word.split('\n') - - if (splitWords.length === mnemonicLength) { - setWords(splitWords) - } else { - setWords(prev => { - const next = [...prev] - next[index] = word.trim() - return next - }) + const trimmedValue = word.trim() + const splitWords = trimmedValue.split(/\s+/).filter(Boolean) + + if (splitWords.length > 1) { + // Case: Pasted content is a full mnemonic of the expected length + if (splitWords.length === mnemonicLength) { + setWords(splitWords) + return + } + + // Case: Pasted content is larger than the total expected mnemonic length + if (splitWords.length > mnemonicLength) { + showToast({ + title: t( + 'onboarding.import_account.invalid_mnemonic_title', + ), + body: t( + 'onboarding.import_account.invalid_mnemonic_body', + ), + type: 'error', + }) + return + } + + // Case: Pasted content is smaller than the total expected length + const remainingSlots = mnemonicLength - index + + if (splitWords.length <= remainingSlots) { + setWords(prev => { + const next = [...prev] + + splitWords.forEach((w, i) => { + next[index + i] = w + }) + return next + }) + } else { + showToast({ + title: t( + 'onboarding.import_account.insufficient_slots_title', + ), + body: t( + 'onboarding.import_account.insufficient_slots_body', + ), + type: 'error', + }) + } + return } + + setWords(prev => { + const next = [...prev] + + next[index] = word.trim() + return next + }) }, - [mnemonicLength], + [mnemonicLength, showToast, t], ) const goToSearchAccounts = useCallback(() => { From bda94727569187f6578925041679217c6b43f611 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:03:57 +0000 Subject: [PATCH 3/8] fix(onboarding): fix onboarding closure redirection --- apps/mobile/src/hooks/useShowOnboarding.ts | 4 +++- apps/mobile/src/modules/onboarding/hooks/index.ts | 6 +++++- .../onboarding/hooks/useOnboardingStore.ts | 15 +++++++++++++++ .../NameAccountScreen/useNameAccountScreen.ts | 8 +++++++- .../OnboardingScreen/useOnboardingScreen.ts | 8 ++++++-- 5 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/hooks/useShowOnboarding.ts b/apps/mobile/src/hooks/useShowOnboarding.ts index 48a6645b..e4d2049c 100644 --- a/apps/mobile/src/hooks/useShowOnboarding.ts +++ b/apps/mobile/src/hooks/useShowOnboarding.ts @@ -14,10 +14,12 @@ import { useHasNoAccounts, useSelectedAccountAddress, } from '@perawallet/wallet-core-accounts' +import { useIsOnboarding } from '@modules/onboarding/hooks' export const useShowOnboarding = () => { const noAccounts = useHasNoAccounts() const { selectedAccountAddress } = useSelectedAccountAddress() + const { isOnboarding } = useIsOnboarding() - return noAccounts || !selectedAccountAddress + return noAccounts || !selectedAccountAddress || isOnboarding } diff --git a/apps/mobile/src/modules/onboarding/hooks/index.ts b/apps/mobile/src/modules/onboarding/hooks/index.ts index d9fd74cf..7c61968b 100644 --- a/apps/mobile/src/modules/onboarding/hooks/index.ts +++ b/apps/mobile/src/modules/onboarding/hooks/index.ts @@ -10,4 +10,8 @@ limitations under the License */ -export { useOnboardingStore, useShouldPlayConfetti } from './useOnboardingStore' +export { + useOnboardingStore, + useShouldPlayConfetti, + useIsOnboarding, +} from './useOnboardingStore' diff --git a/apps/mobile/src/modules/onboarding/hooks/useOnboardingStore.ts b/apps/mobile/src/modules/onboarding/hooks/useOnboardingStore.ts index c74e6321..68c2669a 100644 --- a/apps/mobile/src/modules/onboarding/hooks/useOnboardingStore.ts +++ b/apps/mobile/src/modules/onboarding/hooks/useOnboardingStore.ts @@ -14,10 +14,12 @@ import { create } from 'zustand' type OnboardingState = { shouldPlayConfetti: boolean + isOnboarding: boolean } type OnboardingActions = { setShouldPlayConfetti: (value: boolean) => void + setIsOnboarding: (value: boolean) => void reset: () => void } @@ -25,12 +27,14 @@ type OnboardingStore = OnboardingState & OnboardingActions const initialState: OnboardingState = { shouldPlayConfetti: false, + isOnboarding: false, } export const useOnboardingStore = create()(set => ({ ...initialState, setShouldPlayConfetti: (value: boolean) => set({ shouldPlayConfetti: value }), + setIsOnboarding: (value: boolean) => set({ isOnboarding: value }), reset: () => set(initialState), })) @@ -50,3 +54,14 @@ export const useShouldPlayConfetti = (): UseShouldPlayConfettiResult => { ) return { shouldPlayConfetti, setShouldPlayConfetti } } + +type UseIsOnboardingResult = { + isOnboarding: boolean + setIsOnboarding: (value: boolean) => void +} + +export const useIsOnboarding = (): UseIsOnboardingResult => { + const isOnboarding = useOnboardingStore(state => state.isOnboarding) + const setIsOnboarding = useOnboardingStore(state => state.setIsOnboarding) + return { isOnboarding, setIsOnboarding } +} diff --git a/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/useNameAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/useNameAccountScreen.ts index e3dd55e0..15a9d3ac 100644 --- a/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/useNameAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/NameAccountScreen/useNameAccountScreen.ts @@ -24,7 +24,10 @@ import { useLanguage } from '@hooks/useLanguage' import { useToast } from '@hooks/useToast' import { useRoute, RouteProp } from '@react-navigation/native' import { OnboardingStackParamList } from '../../routes' -import { useShouldPlayConfetti } from '@modules/onboarding/hooks' +import { + useShouldPlayConfetti, + useIsOnboarding, +} from '@modules/onboarding/hooks' type NameAccountScreenRouteProp = RouteProp< OnboardingStackParamList, @@ -41,6 +44,7 @@ export const useNameAccountScreen = () => { const { t } = useLanguage() const { showToast } = useToast() const { setShouldPlayConfetti } = useShouldPlayConfetti() + const { setIsOnboarding } = useIsOnboarding() const routeAccount = route.params?.account @@ -96,6 +100,8 @@ export const useNameAccountScreen = () => { // Set confetti state - AccountScreen will read this and play the animation setShouldPlayConfetti(true) + + setIsOnboarding(false) } catch (error) { showToast({ title: t('onboarding.create_account.error_title'), diff --git a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts index ffbfb2fc..7159a1e3 100644 --- a/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/OnboardingScreen/useOnboardingScreen.ts @@ -15,6 +15,7 @@ import { useAppNavigation } from '@hooks/useAppNavigation' import { useWebView } from '@modules/webview' import { config } from '@perawallet/wallet-core-config' import { useModalState } from '@hooks/useModalState' +import { useIsOnboarding } from '@modules/onboarding/hooks' export const useOnboardingScreen = () => { const navigation = useAppNavigation() @@ -24,6 +25,7 @@ export const useOnboardingScreen = () => { open: openImportOptions, close: closeImportOptions, } = useModalState() + const { setIsOnboarding } = useIsOnboarding() const handleTermsPress = useCallback(() => { pushWebView({ @@ -45,13 +47,15 @@ export const useOnboardingScreen = () => { const handleHDWalletPress = useCallback(() => { closeImportOptions() + setIsOnboarding(true) navigation.push('ImportInfo', { accountType: 'hdWallet' }) - }, [closeImportOptions, navigation]) + }, [closeImportOptions, navigation, setIsOnboarding]) const handleAlgo25Press = useCallback(() => { closeImportOptions() + setIsOnboarding(true) navigation.push('ImportInfo', { accountType: 'algo25' }) - }, [closeImportOptions, navigation]) + }, [closeImportOptions, navigation, setIsOnboarding]) return { isImportOptionsVisible, From 721dc2f83b897ef38f3d6871edf24238b36784dd Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:06:29 +0000 Subject: [PATCH 4/8] fix(onboarding): fixing dots sizes in the new screen --- .../onboarding/screens/SearchAccountsScreen/styles.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts index df2926ca..dedb754c 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/styles.ts @@ -19,6 +19,7 @@ export const useStyles = makeStyles(theme => ({ justifyContent: 'center', alignItems: 'center', padding: theme.spacing.xl, + gap: theme.spacing.xl, }, topRow: { flexDirection: 'row', @@ -29,12 +30,12 @@ export const useStyles = makeStyles(theme => ({ flexDirection: 'row', alignItems: 'center', marginHorizontal: theme.spacing.xl, - gap: theme.spacing.sm, + gap: theme.spacing.md, }, dot: { - width: 8, - height: 8, - borderRadius: 4, + width: theme.spacing.xs, + height: theme.spacing.xs, + borderRadius: theme.spacing.xs, }, dot1: { backgroundColor: theme.colors.grey2, // Gray 200 @@ -50,6 +51,5 @@ export const useStyles = makeStyles(theme => ({ }, title: { textAlign: 'center', - marginTop: theme.spacing.md, }, })) From ac51710e2af8d96d42ea28b302f927ba36a01a66 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:11:53 +0000 Subject: [PATCH 5/8] feat(onboarding): adding animation to dots while page is loading --- .../src/modules/onboarding/hooks/index.ts | 6 +-- .../SearchAccountsScreen.tsx | 35 ++++++++++++--- .../useSearchAccountsScreen.ts | 43 +++++++++++++++++++ apps/mobile/vitest.setup.ts | 35 +++++++++++---- 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/hooks/index.ts b/apps/mobile/src/modules/onboarding/hooks/index.ts index 7c61968b..250b8198 100644 --- a/apps/mobile/src/modules/onboarding/hooks/index.ts +++ b/apps/mobile/src/modules/onboarding/hooks/index.ts @@ -11,7 +11,7 @@ */ export { - useOnboardingStore, - useShouldPlayConfetti, - useIsOnboarding, + useOnboardingStore, + useShouldPlayConfetti, + useIsOnboarding, } from './useOnboardingStore' diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx index 31e88b0f..b9ee762b 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx @@ -10,14 +10,15 @@ limitations under the License */ +import { Animated } from 'react-native' import { PWText, PWView } from '@components/core' import { RoundButton } from '@components/RoundButton/RoundButton' import { useStyles } from './styles' import { useSearchAccountsScreen } from './useSearchAccountsScreen' export const SearchAccountsScreen = () => { + const { t, dotOpacities } = useSearchAccountsScreen() const styles = useStyles() - const { t } = useSearchAccountsScreen() return ( @@ -28,10 +29,34 @@ export const SearchAccountsScreen = () => { /> - - - - + + + + string + dotOpacities: Animated.Value[] } +const DOT_COUNT = 4 +const ANIMATION_DURATION = 400 +const STEP_DURATION = 500 +const TRANSPARENT_OPACITY = 0.3 +const FULL_OPACITY = 1 + export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { const { t } = useLanguage() + const dotOpacities = useRef( + Array.from( + { length: DOT_COUNT }, + () => new Animated.Value(FULL_OPACITY), + ), + ).current + + useEffect(() => { + let currentIndex = 0 + + // Initialize first dot as transparent + dotOpacities[0].setValue(TRANSPARENT_OPACITY) + + const interval = setInterval(() => { + const prevIndex = currentIndex + currentIndex = (currentIndex + 1) % DOT_COUNT + + Animated.parallel([ + Animated.timing(dotOpacities[prevIndex], { + toValue: FULL_OPACITY, + duration: ANIMATION_DURATION, + useNativeDriver: true, + }), + Animated.timing(dotOpacities[currentIndex], { + toValue: TRANSPARENT_OPACITY, + duration: ANIMATION_DURATION, + useNativeDriver: true, + }), + ]).start() + }, STEP_DURATION) + + return () => clearInterval(interval) + }, [dotOpacities]) return { t, + dotOpacities, } } diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts index 82e9bfee..20ca3411 100644 --- a/apps/mobile/vitest.setup.ts +++ b/apps/mobile/vitest.setup.ts @@ -344,17 +344,36 @@ vi.mock('react-native', () => { quad: vi.fn(), }, Animated: { - timing: vi.fn(() => ({ start: vi.fn() })), - spring: vi.fn(() => ({ start: vi.fn() })), - event: vi.fn(), - Value: vi.fn(() => ({ - setValue: vi.fn(), - interpolate: vi.fn(() => '0px'), + timing: vi.fn(() => ({ start: vi.fn(cb => cb?.()) })), + spring: vi.fn(() => ({ start: vi.fn(cb => cb?.()) })), + parallel: vi.fn(animations => ({ + start: vi.fn(cb => { + animations.forEach((a: any) => a.start()) + cb?.() + }), })), + event: vi.fn(), + Value: vi.fn(function (this: any) { + this.setValue = vi.fn() + this.interpolate = vi.fn(() => '0px') + }), createAnimatedComponent: vi.fn(c => c), - View: vi.fn(({ children }) => children), - Text: vi.fn(({ children }) => children), + View: vi.fn(({ children, style, ...props }) => + require('react').createElement( + 'div', + { ...props, style }, + children, + ), + ), + Text: vi.fn(({ children, style, ...props }) => + require('react').createElement( + 'span', + { ...props, style }, + children, + ), + ), }, + PixelRatio: { get: vi.fn(() => 1), getFontScale: vi.fn(() => 1), From 4cea6f14ccff628d703a3fe19a7ff0c6d51ad2e2 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 14:37:27 +0000 Subject: [PATCH 6/8] feat(onboarding): adding logic to find account derivations --- apps/mobile/src/i18n/locales/en.json | 3 + .../src/modules/onboarding/routes/index.tsx | 11 ++ .../src/modules/onboarding/routes/types.ts | 18 +- .../useImportAccountScreen.ts | 29 +++- .../ImportSelectAddressesScreen.tsx | 39 +++++ .../ImportSelectAddressesScreen.spec.tsx | 48 ++++++ .../ImportSelectAddressesScreen/index.ts | 13 ++ .../ImportSelectAddressesScreen/styles.ts | 25 +++ .../useImportSelectAddressesScreen.ts | 40 +++++ .../__tests__/SearchAccountsScreen.spec.tsx | 51 ++++++ .../useSearchAccountsScreen.ts | 155 +++++++++++++++++- apps/mobile/vitest.setup.ts | 54 +++--- 12 files changed, 444 insertions(+), 42 deletions(-) create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/__tests__/ImportSelectAddressesScreen.spec.tsx create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/index.ts create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts create mode 100644 apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 33d8c20d..09abb4d5 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -326,6 +326,9 @@ "wallet_label": "Wallet #{{count}}", "input_label": "Account name", "finish_button": "Finish Account Creation" + }, + "import_select_addresses": { + "title": "Select your accounts" } }, "portfolio": { diff --git a/apps/mobile/src/modules/onboarding/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx index 2ff8920e..81141bb7 100644 --- a/apps/mobile/src/modules/onboarding/routes/index.tsx +++ b/apps/mobile/src/modules/onboarding/routes/index.tsx @@ -20,6 +20,7 @@ import { OnboardingScreen } from '@modules/onboarding/screens/OnboardingScreen' import { NameAccountScreen } from '@modules/onboarding/screens/NameAccountScreen' import { ImportAccountScreen } from '@modules/onboarding/screens/ImportAccountScreen' import { ImportInfoScreen } from '@modules/onboarding/screens/ImportInfoScreen' +import { ImportSelectAddressesScreen } from '@modules/onboarding/screens/ImportSelectAddressesScreen' import { AccountErrorBoundary } from '@modules/accounts/components/AccountErrorBoundary/AccountErrorBoundary' import { useLanguage } from '@hooks/useLanguage' import { screenListeners } from '@routes/listeners' @@ -52,6 +53,8 @@ const ImportInfoScreenWithErrorBoundary = withAccountErrorBoundary(ImportInfoScreen) const SearchAccountsScreenWithErrorBoundary = withAccountErrorBoundary(SearchAccountsScreen) +const ImportSelectAddressesScreenWithErrorBoundary = + withAccountErrorBoundary(ImportSelectAddressesScreen) import { OnboardingStackParamList } from './types' export type { OnboardingStackParamList } from './types' @@ -109,6 +112,14 @@ export const OnboardingStackNavigator = () => { layout={fullScreenLayout} component={SearchAccountsScreenWithErrorBoundary} /> + ) } diff --git a/apps/mobile/src/modules/onboarding/routes/types.ts b/apps/mobile/src/modules/onboarding/routes/types.ts index f0888128..a2199d5d 100644 --- a/apps/mobile/src/modules/onboarding/routes/types.ts +++ b/apps/mobile/src/modules/onboarding/routes/types.ts @@ -12,21 +12,29 @@ import { WalletAccount, + HDWalletAccount, ImportAccountType, } from '@perawallet/wallet-core-accounts' export type OnboardingStackParamList = { OnboardingHome: undefined NameAccount: - | { - account?: WalletAccount - } - | undefined + | { + account?: WalletAccount + } + | undefined + ImportSelectAddresses: { + accounts: HDWalletAccount[] + } + ImportInfo: { accountType: ImportAccountType } ImportAccount: { accountType: ImportAccountType } - SearchAccounts: undefined + SearchAccounts: { + account: HDWalletAccount + } } + diff --git a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts index df1d0888..11c6303c 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportAccountScreen/useImportAccountScreen.ts @@ -17,7 +17,9 @@ import { OnboardingStackParamList } from '../../routes/types' import { useImportAccount, ImportAccountType, + AccountTypes, } from '@perawallet/wallet-core-accounts' + import { useToast } from '@hooks/useToast' import { useLanguage } from '@hooks/useLanguage' import { useAppNavigation } from '@hooks/useAppNavigation' @@ -146,19 +148,21 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { [mnemonicLength, showToast, t], ) - const goToSearchAccounts = useCallback(() => { - navigation.replace('SearchAccounts') - }, [navigation]) - const handleImportAccount = useCallback(() => { setProcessing(true) setTimeout(async () => { + const mnemonic = words.join(' ') + try { - await importAccount({ - mnemonic: words.join(' '), + const importedAccount = await importAccount({ + mnemonic, type: accountType, }) - goToSearchAccounts() + + if (importedAccount.type === AccountTypes.hdWallet) { + navigation.push('SearchAccounts', { account: importedAccount }) + } + // TODO: Algo25 will come later } catch { showToast({ title: t('onboarding.import_account.failed_title'), @@ -169,7 +173,16 @@ export function useImportAccountScreen(): UseImportAccountScreenResult { setProcessing(false) } }, 0) - }, [importAccount, words, goToSearchAccounts, showToast, t]) + }, [ + importAccount, + words, + accountType, + navigation, + showToast, + t, + ]) + + return { words, diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx new file mode 100644 index 00000000..a4cea145 --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx @@ -0,0 +1,39 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import React from 'react' +import { PWView, PWText, PWToolbar, PWIcon, PWTouchableOpacity } from '@components/core' +import { useStyles } from './styles' +import { useImportSelectAddressesScreen } from './useImportSelectAddressesScreen' +import { useAppNavigation } from '@hooks/useAppNavigation' + +export const ImportSelectAddressesScreen = () => { + const styles = useStyles() + const { accounts, t } = useImportSelectAddressesScreen() + const navigation = useAppNavigation() + + return ( + + + + + } + /> + + {t('onboarding.import_select_addresses.title')} + {/* List of accounts will be implemented here */} + + + ) +} diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/__tests__/ImportSelectAddressesScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/__tests__/ImportSelectAddressesScreen.spec.tsx new file mode 100644 index 00000000..2da7516b --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/__tests__/ImportSelectAddressesScreen.spec.tsx @@ -0,0 +1,48 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { render, screen } from '@test-utils/render' +import { vi } from 'vitest' +import { useRoute } from '@react-navigation/native' +import { ImportSelectAddressesScreen } from '../ImportSelectAddressesScreen' +import { AccountTypes } from '@perawallet/wallet-core-accounts' + +describe('ImportSelectAddressesScreen', () => { + beforeEach(() => { + vi.mocked(useRoute).mockReturnValue({ + params: { + accounts: [ + { + id: '1', + address: 'MOCK_ADDRESS', + type: AccountTypes.hdWallet, + canSign: true, + hdWalletDetails: { + walletId: '1', + account: 0, + change: 0, + keyIndex: 0, + derivationType: 9, + }, + }, + ], + }, + } as any) + }) + + it('renders the select addresses title', () => { + render() + expect( + screen.getByText('onboarding.import_select_addresses.title'), + ).toBeTruthy() + }) +}) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/index.ts b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/index.ts new file mode 100644 index 00000000..52f50bdb --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/index.ts @@ -0,0 +1,13 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +export { ImportSelectAddressesScreen } from './ImportSelectAddressesScreen' diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts new file mode 100644 index 00000000..dbb9cb8a --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts @@ -0,0 +1,25 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { makeStyles } from '@rneui/themed' + +export const useStyles = makeStyles(theme => ({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + content: { + flex: 1, + paddingHorizontal: theme.spacing.xl, + paddingTop: theme.spacing.xl, + }, +})) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts new file mode 100644 index 00000000..56a96024 --- /dev/null +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts @@ -0,0 +1,40 @@ +/* + Copyright 2022-2025 Pera Wallet, LDA + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License + */ + +import { RouteProp, useRoute } from '@react-navigation/native' +import { OnboardingStackParamList } from '../../routes/types' +import { HDWalletAccount } from '@perawallet/wallet-core-accounts' +import { useLanguage } from '@hooks/useLanguage' +import { useAppNavigation } from '@hooks/useAppNavigation' + +type ImportSelectAddressesRouteProp = RouteProp< + OnboardingStackParamList, + 'ImportSelectAddresses' +> + +export type UseImportSelectAddressesScreenResult = { + accounts: HDWalletAccount[] + t: (key: string) => string +} + +export function useImportSelectAddressesScreen(): UseImportSelectAddressesScreenResult { + const { + params: { accounts }, + } = useRoute() + const { t } = useLanguage() + const navigation = useAppNavigation() + + return { + accounts, + t, + } +} diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx index 2813a5e0..8c1b688a 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx @@ -11,9 +11,59 @@ */ import { render, screen } from '@test-utils/render' +import { vi } from 'vitest' +import { useRoute } from '@react-navigation/native' import { SearchAccountsScreen } from '../SearchAccountsScreen' +import { AccountTypes } from '@perawallet/wallet-core-accounts' + +// Mock the hooks to avoid actual blockchain/KMS calls during tests +vi.mock('@perawallet/wallet-core-accounts', async (importOriginal) => ({ + ...(await importOriginal()), + useHDWallet: () => ({ + deriveAccountAddress: vi.fn(), + }), + useCreateAccount: () => vi.fn(), +})) + +vi.mock('@perawallet/wallet-core-kms', () => ({ + useKMS: () => ({ + getPrivateData: vi.fn(), + }), +})) + +vi.mock('@perawallet/wallet-core-blockchain', () => ({ + useAlgorandClient: () => ({ + client: { + algod: { + accountInformation: vi.fn(() => ({})), + + }, + }, + }), + encodeAlgorandAddress: vi.fn(() => 'MOCK_ADDRESS'), +})) describe('SearchAccountsScreen', () => { + beforeEach(() => { + vi.mocked(useRoute).mockReturnValue({ + params: { + account: { + id: '1', + address: 'MOCK_ADDRESS', + type: AccountTypes.hdWallet, + canSign: true, + hdWalletDetails: { + walletId: '1', + account: 0, + change: 0, + keyIndex: 0, + derivationType: 9, + }, + }, + }, + } as any) + }) + it('renders searching accounts title', () => { render() expect( @@ -30,3 +80,4 @@ describe('SearchAccountsScreen', () => { expect(screen.getByTestId('icon-phone')).toBeTruthy() }) }) + diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts index f9f97e0e..213f3f7c 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts @@ -10,9 +10,26 @@ limitations under the License */ -import { useEffect, useRef } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { Animated } from 'react-native' import { useLanguage } from '@hooks/useLanguage' +import { useAppNavigation } from '@hooks/useAppNavigation' +import { RouteProp, useRoute } from '@react-navigation/native' +import { v7 as uuidv7 } from 'uuid' +import { + useHDWallet, + getSeedFromMasterKey, + AccountTypes, + type WalletAccount, + type HDWalletAccount, +} from '@perawallet/wallet-core-accounts' +import { BIP32DerivationType } from '@algorandfoundation/xhd-wallet-api' +import { useKMS } from '@perawallet/wallet-core-kms' +import { + useAlgorandClient, + encodeAlgorandAddress, +} from '@perawallet/wallet-core-blockchain' +import { OnboardingStackParamList } from '../../routes/types' export type UseSearchAccountsScreenResult = { t: (key: string) => string @@ -25,19 +42,29 @@ const STEP_DURATION = 500 const TRANSPARENT_OPACITY = 0.3 const FULL_OPACITY = 1 +const MAX_ACCOUNT_GAP = 5 +const MAX_KEY_INDEX_GAP = 5 +const MAX_SEARCH_DEPTH = 20 + export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { + const { + params: { account }, + } = useRoute>() const { t } = useLanguage() + const navigation = useAppNavigation() + const { getPrivateData } = useKMS() + const { deriveAccountAddress } = useHDWallet() + const algorandClient = useAlgorandClient() + + const onboardingWalletId = account.hdWalletDetails.walletId; + const dotOpacities = useRef( - Array.from( - { length: DOT_COUNT }, - () => new Animated.Value(FULL_OPACITY), - ), + Array.from({ length: DOT_COUNT }, () => new Animated.Value(FULL_OPACITY)), ).current useEffect(() => { let currentIndex = 0 - // Initialize first dot as transparent dotOpacities[0].setValue(TRANSPARENT_OPACITY) const interval = setInterval(() => { @@ -61,8 +88,124 @@ export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { return () => clearInterval(interval) }, [dotOpacities]) + const searchAccounts = useCallback(async () => { + if (!onboardingWalletId) { + return + } + + try { + const privateData = await getPrivateData(onboardingWalletId) + + if (!privateData) { + return + } + + const seed = getSeedFromMasterKey(privateData) + const foundAccounts: HDWalletAccount[] = [account] + + let accountGap = 0 + + for ( + let accountIndex = 0; + accountIndex < MAX_SEARCH_DEPTH; + accountIndex++ + ) { + if (accountGap >= MAX_ACCOUNT_GAP) { + break + } + + let keyIndexGap = 0 + let accountHasActivity = false + + for ( + let keyIndex = 0; + keyIndex < MAX_SEARCH_DEPTH; + keyIndex++ + ) { + if (keyIndexGap >= MAX_KEY_INDEX_GAP) { + break + } + + // Skip 0/0 as it was already imported in the previous step + if (accountIndex === 0 && keyIndex === 0) { + keyIndexGap = 0 + continue + } + + const { address } = await deriveAccountAddress({ + seed, + account: accountIndex, + keyIndex, + }) + + const encodedAddress = encodeAlgorandAddress(address) + const accountInfo = + await algorandClient.client.algod.accountInformation(encodedAddress) + + const hasActivity = + accountInfo.amount > 0 || + (accountInfo.assets && accountInfo.assets.length > 0) || + (accountInfo.appsLocalState && + accountInfo.appsLocalState.length > 0) || + (accountInfo.appsTotalSchema && + ((accountInfo.appsTotalSchema.numUints ?? 0) > 0 || + (accountInfo.appsTotalSchema.numByteSlices ?? 0) > 0)) + + if (hasActivity) { + accountHasActivity = true + keyIndexGap = 0 + + const newAccount: WalletAccount = { + id: uuidv7(), + address: encodedAddress, + type: AccountTypes.hdWallet, + canSign: true, + hdWalletDetails: { + walletId: onboardingWalletId!, + account: accountIndex, + change: 0, + keyIndex, + derivationType: BIP32DerivationType.Peikert, + }, + } + + foundAccounts.push(newAccount) + } else { + keyIndexGap++ + } + } + + if (accountHasActivity) { + accountGap = 0 + } else { + // We only count an account as "empty" if even its 0th key index is empty + // This matches the typical discovery logic + accountGap++ + } + } + + navigation.replace('ImportSelectAddresses', { accounts: foundAccounts }) + } catch { + // Error handling could be added here (e.g. show toast) + // For now we just stop searching + } + }, [ + onboardingWalletId, + getPrivateData, + deriveAccountAddress, + algorandClient, + navigation, + account, + ]) + + useEffect(() => { + searchAccounts() + }, [searchAccounts]) + return { t, dotOpacities, + isSearching, } } + diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts index 20ca3411..077d5c5d 100644 --- a/apps/mobile/vitest.setup.ts +++ b/apps/mobile/vitest.setup.ts @@ -22,7 +22,7 @@ vi.mock('react-native-reanimated', () => { const React = require('react') const Reanimated = { default: { - call: () => {}, + call: () => { }, createAnimatedComponent: (component: any) => component, View: (props: any) => React.createElement('div', props, props.children), @@ -31,16 +31,16 @@ vi.mock('react-native-reanimated', () => { Image: (props: any) => React.createElement('img', props), ScrollView: (props: any) => React.createElement('div', props, props.children), - addWhitelistedNativeProps: () => {}, - addWhitelistedUIProps: () => {}, + addWhitelistedNativeProps: () => { }, + addWhitelistedUIProps: () => { }, }, useSharedValue: (v: any) => ({ value: v }), useDerivedValue: (a: any) => ({ value: a() }), useAnimatedStyle: () => ({}), useAnimatedProps: () => ({}), - useAnimatedGestureHandler: () => {}, - useAnimatedScrollHandler: () => {}, - useAnimatedReaction: () => {}, + useAnimatedGestureHandler: () => { }, + useAnimatedScrollHandler: () => { }, + useAnimatedReaction: () => { }, withTiming: (toValue: any) => toValue, withSpring: (toValue: any) => toValue, withDecay: () => 0, @@ -50,7 +50,7 @@ vi.mock('react-native-reanimated', () => { runOnJS: (fn: any) => fn, runOnUI: (fn: any) => fn, makeMutable: (v: any) => ({ value: v }), - cancelAnimation: () => {}, + cancelAnimation: () => { }, interpolate: () => 0, Extrapolate: { CLAMP: 'clamp' }, Layout: { @@ -418,7 +418,7 @@ vi.mock('react-native-quick-crypto', () => ({ // Basic NativeEventEmitter dependency to avoid errors when no native module is provided vi.mock('react-native/Libraries/EventEmitter/NativeEventEmitter', () => { - return class NativeEventEmitter {} + return class NativeEventEmitter { } }) // Mock React Navigation @@ -429,9 +429,9 @@ vi.mock('@react-navigation/native', () => ({ reset: vi.fn(), setOptions: vi.fn(), }), - useRoute: () => ({ + useRoute: vi.fn(() => ({ params: {}, - }), + })), useFocusEffect: vi.fn(), NavigationContainer: ({ children }: any) => children, DefaultTheme: { @@ -483,7 +483,7 @@ vi.mock('@react-native-firebase/messaging', () => ({ vi.mock('@react-native-firebase/remote-config', () => ({ getRemoteConfig: () => ({ - setDefaults: vi.fn(async () => {}), + setDefaults: vi.fn(async () => { }), fetchAndActivate: vi.fn(async () => true), setConfigSettings: vi.fn(), getValue: vi.fn(() => ({ @@ -691,10 +691,10 @@ vi.mock('@rneui/themed', () => { ({ isVisible, children, ...props }: any) => isVisible ? React.createElement( - MockView, - { ...props, 'data-testid': 'Dialog' }, - children, - ) + MockView, + { ...props, 'data-testid': 'Dialog' }, + children, + ) : null, { Title: DialogTitle, @@ -768,18 +768,18 @@ vi.mock('@rneui/themed', () => { BottomSheet: ({ isVisible, children, ...props }: any) => isVisible ? React.createElement( - MockView, - { ...props, 'data-testid': 'RNEBottomSheet' }, - children, - ) + MockView, + { ...props, 'data-testid': 'RNEBottomSheet' }, + children, + ) : null, Overlay: ({ isVisible, children, ...props }: any) => isVisible ? React.createElement( - MockView, - { ...props, 'data-testid': 'RNEOverlay' }, - children, - ) + MockView, + { ...props, 'data-testid': 'RNEOverlay' }, + children, + ) : null, Icon: (props: any) => React.createElement(MockView, props), Tab, @@ -988,7 +988,15 @@ vi.mock('@perawallet/wallet-core-accounts', () => { isPending: false, })), ALGO_ASSET_ID: '0', + AccountTypes: { + algo25: 'algo25', + hdWallet: 'hdWallet', + hardware: 'hardware', + multisig: 'multisig', + watch: 'watch', + }, } + }) // Mock @perawallet/wallet-core-contacts From 5bb7a794d1285e99a66e94e160e7e0e3a62f6f25 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:00:29 +0000 Subject: [PATCH 7/8] chore(onboarding): adjusting exports and returns --- .../screens/SearchAccountsScreen/useSearchAccountsScreen.ts | 1 - packages/accounts/src/index.ts | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts index 213f3f7c..51ee1fc1 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts @@ -205,7 +205,6 @@ export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { return { t, dotOpacities, - isSearching, } } diff --git a/packages/accounts/src/index.ts b/packages/accounts/src/index.ts index 4db03579..f5caf802 100644 --- a/packages/accounts/src/index.ts +++ b/packages/accounts/src/index.ts @@ -17,4 +17,5 @@ export * from './hooks' export * from './errors' export * from './utils' -export { initAccountsStore } from './store' +export { initAccountsStore, useAccountsStore } from './store' + From b42a068b491b6e26883327233c980a7d5ccb42f1 Mon Sep 17 00:00:00 2001 From: Fred Souza <1396615+fmsouza@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:16:57 +0000 Subject: [PATCH 8/8] feat(onboarding): implementing contents of the new Import select addresses screen --- apps/mobile/src/i18n/locales/en.json | 7 +- .../ImportSelectAddressesScreen.tsx | 113 +++++++++++++++++- .../ImportSelectAddressesScreen/styles.ts | 66 +++++++++- .../useImportSelectAddressesScreen.ts | 77 +++++++++++- .../useSearchAccountsScreen.ts | 8 +- apps/mobile/vitest.setup.ts | 11 ++ 6 files changed, 269 insertions(+), 13 deletions(-) diff --git a/apps/mobile/src/i18n/locales/en.json b/apps/mobile/src/i18n/locales/en.json index 09abb4d5..5253692e 100644 --- a/apps/mobile/src/i18n/locales/en.json +++ b/apps/mobile/src/i18n/locales/en.json @@ -328,7 +328,12 @@ "finish_button": "Finish Account Creation" }, "import_select_addresses": { - "title": "Select your accounts" + "title": "Select addresses to add", + "description": "We found that there are {{count}} addresses registered to this wallet", + "addresses_count": "{{count}} addresses", + "select_all": "Select all", + "already_imported": "already imported", + "continue": "Continue" } }, "portfolio": { diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx index a4cea145..bdfae32b 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx @@ -11,16 +11,75 @@ */ import React from 'react' -import { PWView, PWText, PWToolbar, PWIcon, PWTouchableOpacity } from '@components/core' +import { FlatList } from 'react-native' +import { + PWView, + PWText, + PWToolbar, + PWIcon, + PWTouchableOpacity, + PWButton, + PWCheckbox, + PWChip, +} from '@components/core' + import { useStyles } from './styles' import { useImportSelectAddressesScreen } from './useImportSelectAddressesScreen' import { useAppNavigation } from '@hooks/useAppNavigation' +import { + getAccountDisplayName, + HDWalletAccount, +} from '@perawallet/wallet-core-accounts' export const ImportSelectAddressesScreen = () => { const styles = useStyles() - const { accounts, t } = useImportSelectAddressesScreen() + const { + accounts, + selectedAddresses, + isAllSelected, + alreadyImportedAddresses, + toggleSelection, + toggleSelectAll, + handleContinue, + t, + } = useImportSelectAddressesScreen() const navigation = useAppNavigation() + const renderItem = ({ item }: { item: HDWalletAccount }) => { + const isImported = alreadyImportedAddresses.has(item.address) + const isSelected = selectedAddresses.has(item.address) + const displayName = getAccountDisplayName(item) + + return ( + toggleSelection(item.address)} + disabled={isImported} + > + + + + {displayName} + + + + + {isImported ? ( + + ) : ( + toggleSelection(item.address)} + containerStyle={styles.checkboxContainer} + /> + )} + + ) + } + return ( { } /> + - {t('onboarding.import_select_addresses.title')} - {/* List of accounts will be implemented here */} + + {t('onboarding.import_select_addresses.title')} + + + {t('onboarding.import_select_addresses.description', { + count: accounts.length, + })} + + + + + {t('onboarding.import_select_addresses.addresses_count', { + count: accounts.length, + })} + + + + + {t('onboarding.import_select_addresses.select_all')} + + + + + + item.address} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + /> + + + + ) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts index dbb9cb8a..3d3b073a 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts @@ -20,6 +20,70 @@ export const useStyles = makeStyles(theme => ({ content: { flex: 1, paddingHorizontal: theme.spacing.xl, - paddingTop: theme.spacing.xl, + }, + title: { + marginBottom: theme.spacing.sm, + marginTop: theme.spacing.sm, + }, + description: { + marginBottom: theme.spacing.xl, + color: theme.colors.textGray, + }, + headerRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.layerGrayLighter, + }, + headerCount: { + color: theme.colors.textGray, + fontWeight: '600', + }, + selectAllContainer: { + flexDirection: 'row', + alignItems: 'center', + }, + selectAllText: { + color: theme.colors.linkPrimary, + marginRight: theme.spacing.xs, + }, + checkboxContainer: { + padding: 0, + margin: 0, + marginLeft: 0, + marginRight: 0, + backgroundColor: 'transparent', + }, + listContent: { + paddingBottom: theme.spacing.xl, + }, + itemContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: theme.spacing.md, + borderBottomWidth: 1, + borderBottomColor: theme.colors.layerGrayLighter, + }, + itemTextContainer: { + flex: 1, + paddingRight: theme.spacing.md, + }, + itemTitle: { + color: theme.colors.textMain, + marginBottom: 2, + }, + itemSubtitle: { + color: theme.colors.textGray, + fontSize: 12, + }, + footer: { + padding: theme.spacing.xl, + borderTopWidth: 1, + borderTopColor: theme.colors.layerGrayLighter, + backgroundColor: theme.colors.background, + paddingBottom: theme.spacing.xl + 20, // Extra padding for safe area logic usually handled by SafeAreaView but good to have buffer }, })) diff --git a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts index 56a96024..c00cdc8c 100644 --- a/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts @@ -10,11 +10,17 @@ limitations under the License */ +import { useState, useMemo, useCallback } from 'react' import { RouteProp, useRoute } from '@react-navigation/native' import { OnboardingStackParamList } from '../../routes/types' -import { HDWalletAccount } from '@perawallet/wallet-core-accounts' +import { + useAllAccounts, + useAccountsStore, + HDWalletAccount, +} from '@perawallet/wallet-core-accounts' import { useLanguage } from '@hooks/useLanguage' -import { useAppNavigation } from '@hooks/useAppNavigation' +import { useIsOnboarding } from '@modules/onboarding/hooks' + type ImportSelectAddressesRouteProp = RouteProp< OnboardingStackParamList, @@ -23,18 +29,81 @@ type ImportSelectAddressesRouteProp = RouteProp< export type UseImportSelectAddressesScreenResult = { accounts: HDWalletAccount[] - t: (key: string) => string + selectedAddresses: Set + isAllSelected: boolean + alreadyImportedAddresses: Set + toggleSelection: (address: string) => void + toggleSelectAll: () => void + handleContinue: () => void + t: (key: string, options?: any) => string } + export function useImportSelectAddressesScreen(): UseImportSelectAddressesScreenResult { const { params: { accounts }, } = useRoute() const { t } = useLanguage() - const navigation = useAppNavigation() + const allAccounts = useAllAccounts() + + const { setIsOnboarding } = useIsOnboarding() + + const alreadyImportedAddresses = useMemo(() => { + return new Set(allAccounts.map(acc => acc.address)) + }, [allAccounts]) + + const selectableAccounts = useMemo(() => { + return accounts.filter(acc => !alreadyImportedAddresses.has(acc.address)) + }, [accounts, alreadyImportedAddresses]) + + const [selectedAddresses, setSelectedAddresses] = useState>( + new Set(selectableAccounts.map(acc => acc.address)), + ) + + const isAllSelected = selectableAccounts.length > 0 && selectedAddresses.size === selectableAccounts.length + + const toggleSelection = useCallback((address: string) => { + if (alreadyImportedAddresses.has(address)) return + + setSelectedAddresses(prev => { + const next = new Set(prev) + if (next.has(address)) { + next.delete(address) + } else { + next.add(address) + } + return next + }) + }, [alreadyImportedAddresses]) + + const toggleSelectAll = useCallback(() => { + if (isAllSelected) { + setSelectedAddresses(new Set()) + } else { + setSelectedAddresses(new Set(selectableAccounts.map(acc => acc.address))) + } + }, [isAllSelected, selectableAccounts]) + + const handleContinue = useCallback(() => { + const accountsToAdd = accounts.filter(acc => selectedAddresses.has(acc.address)) + + if (accountsToAdd.length > 0) { + const { setAccounts } = useAccountsStore.getState() + setAccounts([...allAccounts, ...accountsToAdd]) + } + + setIsOnboarding(false) + }, [accounts, selectedAddresses, allAccounts, setIsOnboarding]) return { accounts, + selectedAddresses, + isAllSelected, + alreadyImportedAddresses, + toggleSelection, + toggleSelectAll, + handleContinue, t, } } + diff --git a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts index 51ee1fc1..9bd195c9 100644 --- a/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts +++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts @@ -10,7 +10,7 @@ limitations under the License */ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { Animated } from 'react-native' import { useLanguage } from '@hooks/useLanguage' import { useAppNavigation } from '@hooks/useAppNavigation' @@ -23,7 +23,6 @@ import { type WalletAccount, type HDWalletAccount, } from '@perawallet/wallet-core-accounts' -import { BIP32DerivationType } from '@algorandfoundation/xhd-wallet-api' import { useKMS } from '@perawallet/wallet-core-kms' import { useAlgorandClient, @@ -101,6 +100,7 @@ export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { } const seed = getSeedFromMasterKey(privateData) + const derivationType = account.hdWalletDetails.derivationType const foundAccounts: HDWalletAccount[] = [account] let accountGap = 0 @@ -136,6 +136,7 @@ export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { seed, account: accountIndex, keyIndex, + derivationType, }) const encodedAddress = encodeAlgorandAddress(address) @@ -165,10 +166,11 @@ export function useSearchAccountsScreen(): UseSearchAccountsScreenResult { account: accountIndex, change: 0, keyIndex, - derivationType: BIP32DerivationType.Peikert, + derivationType, }, } + foundAccounts.push(newAccount) } else { keyIndexGap++ diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts index 077d5c5d..1f5eff02 100644 --- a/apps/mobile/vitest.setup.ts +++ b/apps/mobile/vitest.setup.ts @@ -284,11 +284,22 @@ vi.mock('react-native', () => { .mockImplementation(props => require('react').createElement('div', props, props.children), ), + FlatList: vi.fn().mockImplementation(({ data, renderItem, ...props }) => { + const React = require('react') + return React.createElement( + 'div', + { ...props, 'data-testid': 'FlatList' }, + data?.map((item: any, index: number) => + renderItem({ item, index }), + ), + ) + }), TextInput: vi .fn() .mockImplementation(props => require('react').createElement('input', props, props.children), ), + Modal: vi .fn() .mockImplementation(props =>