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 =>