()(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/routes/index.tsx b/apps/mobile/src/modules/onboarding/routes/index.tsx
index 7d1cc066..81141bb7 100644
--- a/apps/mobile/src/modules/onboarding/routes/index.tsx
+++ b/apps/mobile/src/modules/onboarding/routes/index.tsx
@@ -20,12 +20,15 @@ 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'
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 +51,10 @@ const ImportAccountScreenWithErrorBoundary =
withAccountErrorBoundary(ImportAccountScreen)
const ImportInfoScreenWithErrorBoundary =
withAccountErrorBoundary(ImportInfoScreen)
+const SearchAccountsScreenWithErrorBoundary =
+ withAccountErrorBoundary(SearchAccountsScreen)
+const ImportSelectAddressesScreenWithErrorBoundary =
+ withAccountErrorBoundary(ImportSelectAddressesScreen)
import { OnboardingStackParamList } from './types'
export type { OnboardingStackParamList } from './types'
@@ -97,6 +104,22 @@ 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..a2199d5d 100644
--- a/apps/mobile/src/modules/onboarding/routes/types.ts
+++ b/apps/mobile/src/modules/onboarding/routes/types.ts
@@ -12,20 +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: {
+ account: HDWalletAccount
+ }
}
+
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 068f230a..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'
@@ -86,36 +88,81 @@ 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 goToHome = useCallback(() => {
- navigation.replace('TabBar', {
- screen: 'Home',
- })
- }, [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,
})
- goToHome()
+
+ if (importedAccount.type === AccountTypes.hdWallet) {
+ navigation.push('SearchAccounts', { account: importedAccount })
+ }
+ // TODO: Algo25 will come later
} catch {
showToast({
title: t('onboarding.import_account.failed_title'),
@@ -126,7 +173,16 @@ export function useImportAccountScreen(): UseImportAccountScreenResult {
setProcessing(false)
}
}, 0)
- }, [importAccount, words, goToHome, 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..bdfae32b
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/ImportSelectAddressesScreen.tsx
@@ -0,0 +1,144 @@
+/*
+ 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 { 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,
+ 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')}
+
+
+ {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/__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..3d3b073a
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/styles.ts
@@ -0,0 +1,89 @@
+/*
+ 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,
+ },
+ 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
new file mode 100644
index 00000000..c00cdc8c
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/ImportSelectAddressesScreen/useImportSelectAddressesScreen.ts
@@ -0,0 +1,109 @@
+/*
+ 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 { useState, useMemo, useCallback } from 'react'
+import { RouteProp, useRoute } from '@react-navigation/native'
+import { OnboardingStackParamList } from '../../routes/types'
+import {
+ useAllAccounts,
+ useAccountsStore,
+ HDWalletAccount,
+} from '@perawallet/wallet-core-accounts'
+import { useLanguage } from '@hooks/useLanguage'
+import { useIsOnboarding } from '@modules/onboarding/hooks'
+
+
+type ImportSelectAddressesRouteProp = RouteProp<
+ OnboardingStackParamList,
+ 'ImportSelectAddresses'
+>
+
+export type UseImportSelectAddressesScreenResult = {
+ accounts: HDWalletAccount[]
+ 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 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/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,
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..b9ee762b
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/SearchAccountsScreen.tsx
@@ -0,0 +1,76 @@
+/*
+ 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 { 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()
+
+ 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..8c1b688a
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/__tests__/SearchAccountsScreen.spec.tsx
@@ -0,0 +1,83 @@
+/*
+ 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 { 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(
+ 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..dedb754c
--- /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,
+ gap: theme.spacing.xl,
+ },
+ topRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginBottom: theme.spacing.xl,
+ },
+ dotsContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginHorizontal: theme.spacing.xl,
+ gap: theme.spacing.md,
+ },
+ dot: {
+ width: theme.spacing.xs,
+ height: theme.spacing.xs,
+ borderRadius: theme.spacing.xs,
+ },
+ 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',
+ },
+}))
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..9bd195c9
--- /dev/null
+++ b/apps/mobile/src/modules/onboarding/screens/SearchAccountsScreen/useSearchAccountsScreen.ts
@@ -0,0 +1,212 @@
+/*
+ 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 { useCallback, useEffect, useRef } 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 { 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
+ dotOpacities: Animated.Value[]
+}
+
+const DOT_COUNT = 4
+const ANIMATION_DURATION = 400
+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)),
+ ).current
+
+ useEffect(() => {
+ let currentIndex = 0
+
+ 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])
+
+ const searchAccounts = useCallback(async () => {
+ if (!onboardingWalletId) {
+ return
+ }
+
+ try {
+ const privateData = await getPrivateData(onboardingWalletId)
+
+ if (!privateData) {
+ return
+ }
+
+ const seed = getSeedFromMasterKey(privateData)
+ const derivationType = account.hdWalletDetails.derivationType
+ 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,
+ derivationType,
+ })
+
+ 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,
+ },
+ }
+
+
+ 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,
+ }
+}
+
diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts
index 82e9bfee..1f5eff02 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: {
@@ -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 =>
@@ -344,17 +355,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),
@@ -399,7 +429,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
@@ -410,9 +440,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: {
@@ -464,7 +494,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(() => ({
@@ -672,10 +702,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,
@@ -749,18 +779,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,
@@ -969,7 +999,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
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'
+