Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7f74be2
feat(accounts): add useLedgerDeviceGroups hook [PERA-XXXX]
fmsouza Apr 27, 2026
121d4eb
test(accounts): broaden useLedgerDeviceGroups exclusion test [PERA-XXXX]
fmsouza Apr 27, 2026
387f956
refactor(accounts): generalize useAccountInfoCard for Ledger structur…
fmsouza Apr 27, 2026
fa9cc06
refactor(accounts): tighten useAccountInfoCard memo dependencies [PER…
fmsouza Apr 27, 2026
81a91b3
chore: format and update copyright header
fmsouza Apr 27, 2026
b17060e
refactor(accounts): tighten useAccountInfoCard memo dependencies [PER…
fmsouza Apr 27, 2026
574802b
fix(accounts): update AccountInfoCard to use correct hook properties …
fmsouza Apr 27, 2026
75605a0
chore: format and update copyright header
fmsouza Apr 27, 2026
68089da
feat(accounts): show Ledger device tree in account more menu [PERA-XXXX]
fmsouza Apr 27, 2026
372a142
refactor(accounts): drop HardwareWalletAccount casts in useAccountInf…
fmsouza Apr 27, 2026
98b2bfd
fix: fixing them provider rendering
fmsouza Apr 27, 2026
f1b7135
feat(ledger): add find_another_wallet i18n key [PERA-XXXX]
fmsouza Apr 27, 2026
b361253
feat(ledger): add FindAnotherWalletRow component [PERA-XXXX]
fmsouza Apr 27, 2026
abd1fac
test(ledger): add failing tests for find-another-wallet hook [PERA-XXXX]
fmsouza Apr 27, 2026
2f5c096
feat(ledger): add handleFindAnother to LedgerSelectAccounts hook [PER…
fmsouza Apr 27, 2026
8a62d7c
feat(ledger): render FindAnotherWalletRow in select-accounts screen […
fmsouza Apr 27, 2026
fedd67c
chore: format and update copyright header
fmsouza Apr 27, 2026
2b330f6
feat(ledger): block Continue while fetching another wallet [PERA-XXXX]
fmsouza Apr 27, 2026
d49048b
chore: format and update copyright header
fmsouza Apr 27, 2026
31b24b3
Merge branch 'main' into fmsouza/pera-3966
fmsouza Apr 27, 2026
7219368
fix(app): resolve merge conflict in App.tsx
fmsouza Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 16 additions & 18 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ const AppContent = () => {
const [bootstrapped, setBootstrapped] = useState(false)
const [fcmToken, setFcmToken] = useState<Nullable<string>>(null)
const { t } = useLanguage()
const isDarkMode = useIsDarkMode()
const bootstrapTheme = getTheme(isDarkMode ? 'dark' : 'light')
const provider = usePeraProvider()
const isDarkMode = useIsDarkMode()
const theme = getTheme(isDarkMode ? 'dark' : 'light')

useEffect(() => {
logger.setErrorReporter(
Expand Down Expand Up @@ -132,22 +132,20 @@ const AppContent = () => {
}, [bootstrapped, provider])

return (
<SafeAreaProvider>
{!bootstrapped && (
<ThemeProvider theme={bootstrapTheme}>
<PWText>{t('common.loading.label')}</PWText>
</ThemeProvider>
)}
{bootstrapped && persister && (
<GestureHandlerRootView>
<NotifierWrapper>
<QueryProvider persister={persister}>
<RootComponent fcmToken={fcmToken} />
</QueryProvider>
</NotifierWrapper>
</GestureHandlerRootView>
)}
</SafeAreaProvider>
<ThemeProvider theme={theme}>
<SafeAreaProvider>
{!bootstrapped && <PWText>{t('common.loading.label')}</PWText>}
{bootstrapped && persister && (
<GestureHandlerRootView>
<NotifierWrapper>
<QueryProvider persister={persister}>
<RootComponent fcmToken={fcmToken} />
</QueryProvider>
</NotifierWrapper>
</GestureHandlerRootView>
)}
</SafeAreaProvider>
</ThemeProvider>
)
}

Expand Down
23 changes: 8 additions & 15 deletions apps/mobile/src/components/RootComponent/RootComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@ import { AppState } from 'react-native'
import { GestureHandlerRootView } from 'react-native-gesture-handler'
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'
import { MainRoutes } from '@routes/index'
import { getTheme } from '@theme/theme'
import { ThemeProvider } from '@rneui/themed'
import { useStyles } from './styles'
import { PWText, PWView } from '@components/core'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import ErrorBoundary from 'react-native-error-boundary'
import { useToast } from '@hooks/useToast'
import { useIsDarkMode } from '@hooks/useIsDarkMode'
import { useDevice } from '@perawallet/wallet-core-device'
import { useNetwork } from '@perawallet/wallet-core-blockchain'
import { useAllAccounts } from '@perawallet/wallet-core-accounts'
Expand Down Expand Up @@ -101,8 +98,6 @@ const RootContentContainer = ({ fcmToken }: RootComponentProps) => {
}

export const RootComponent = ({ fcmToken }: RootComponentProps) => {
const isDarkMode = useIsDarkMode()
const theme = getTheme(isDarkMode ? 'dark' : 'light')
const { network } = useNetwork()
const { registerDevice } = useDevice()
const accounts = useAllAccounts()
Expand Down Expand Up @@ -177,15 +172,13 @@ export const RootComponent = ({ fcmToken }: RootComponentProps) => {
}, [appStatePlatform, accounts, runSyncAction])

return (
<ThemeProvider theme={theme}>
<BottomSheetModalProvider>
<AutoLockGuard>
<WalletConnectProvider>
<RootContentContainer fcmToken={fcmToken} />
</WalletConnectProvider>
<SigningOverlays />
</AutoLockGuard>
</BottomSheetModalProvider>
</ThemeProvider>
<BottomSheetModalProvider>
<AutoLockGuard>
<WalletConnectProvider>
<RootContentContainer fcmToken={fcmToken} />
</WalletConnectProvider>
<SigningOverlays />
</AutoLockGuard>
</BottomSheetModalProvider>
)
}
3 changes: 2 additions & 1 deletion apps/mobile/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1595,7 +1595,8 @@
"select_all": "Select all",
"already_imported": "Already imported",
"continue": "Continue",
"no_new_accounts": "All accounts on this device are already imported."
"no_new_accounts": "All accounts on this device are already imported.",
"find_another_wallet": "Find another wallet"
},
"verify": {
"title": "Verify on Ledger",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { useAccountInfoCard } from './useAccountInfoCard'
import { AccountIcon } from '../AccountIcon'
import { CurrencyDisplay } from '@components/CurrencyDisplay'
import { ExpandablePanel } from '@components/ExpandablePanel'
import { WalletStructureTree } from './WalletStructureTree'
import { AccountStructureTree } from './AccountStructureTree'
import { InfoButton } from '@components/InfoButton'
import { AccountTypeInfoContent } from './AccountTypeInfoContent'
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated'
Expand All @@ -45,10 +45,11 @@ export const AccountInfoCard = ({ account, onClose }: AccountInfoCardProps) => {
accountTypeLabel,
minBalanceAlgos,
isMinBalanceLoading,
isHDWallet,
showMinBalance,
walletLabel,
walletAccounts,
showStructure,
structureLabel,
structureIcon,
structureAccounts,
handleScanAddresses,
} = useAccountInfoCard({ account, onClose })

Expand Down Expand Up @@ -118,13 +119,14 @@ export const AccountInfoCard = ({ account, onClose }: AccountInfoCardProps) => {
</PWView>
)}

{/* Wallet structure (HD wallets only) */}
{isHDWallet && (
{/* Wallet structure (HD wallets and Ledger devices) */}
{showStructure && (
<>
<ExpandablePanel isExpanded={isExpanded}>
<WalletStructureTree
walletLabel={walletLabel}
accounts={walletAccounts}
<AccountStructureTree
label={structureLabel}
icon={structureIcon}
accounts={structureAccounts}
onScanAddresses={handleScanAddresses}
/>
</ExpandablePanel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,45 @@
*/

import {
IconName,
PWIcon,
PWRoundIcon,
PWText,
PWTouchableOpacity,
PWView,
} from '@components/core'
import { CopyableText } from '@components/CopyableText'
import { HDWalletAccount } from '@perawallet/wallet-core-accounts'
import { WalletAccount } from '@perawallet/wallet-core-accounts'
import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'
import { useLanguage } from '@hooks/useLanguage'
import { useStyles } from './styles'
import { AccountIcon } from '../AccountIcon'
type WalletStructureTreeProps = {
walletLabel: string
accounts: HDWalletAccount[]

type AccountStructureTreeProps = {
label: string
icon: IconName
accounts: WalletAccount[]
onScanAddresses: () => void
}

export const WalletStructureTree = ({
walletLabel,
export const AccountStructureTree = ({
label,
icon,
accounts,
onScanAddresses,
}: WalletStructureTreeProps) => {
}: AccountStructureTreeProps) => {
const styles = useStyles()
const { t } = useLanguage()

return (
<PWView style={styles.treeContainer}>
<PWView style={styles.walletRow}>
<PWRoundIcon
icon='wallet'
icon={icon}
variant='secondary'
size='lg'
/>
<PWText variant='body'>{walletLabel}</PWText>
<PWText variant='body'>{label}</PWText>
</PWView>

{accounts.map(account => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { render, screen, fireEvent } from '@test-utils/render'
import { AccountInfoCard } from '../AccountInfoCard'
import {
HDWalletAccount,
HardwareWalletAccount,
WalletAccount,
} from '@perawallet/wallet-core-accounts'
import { Decimal } from 'decimal.js'
Expand Down Expand Up @@ -123,6 +124,7 @@ vi.mock('@perawallet/wallet-core-blockchain', () => ({

const mockUseAccountInformationQuery = vi.fn()
const mockUseHDWalletGroups = vi.fn()
const mockUseLedgerDeviceGroups = vi.fn()
const mockUseAccountLogicalType = vi.fn()
vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
const actual =
Expand All @@ -134,6 +136,7 @@ vi.mock('@perawallet/wallet-core-accounts', async importOriginal => {
useAccountInformationQuery: (...args: unknown[]) =>
mockUseAccountInformationQuery(...args),
useHDWalletGroups: () => mockUseHDWalletGroups(),
useLedgerDeviceGroups: () => mockUseLedgerDeviceGroups(),
useAccountLogicalType: (...args: unknown[]) =>
mockUseAccountLogicalType(...args),
}
Expand Down Expand Up @@ -167,6 +170,18 @@ const watchAccount: WalletAccount = {
address: 'WATCHADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ',
}

const ledgerAccount: HardwareWalletAccount = {
type: 'hardware',
address: 'LEDGERADDR1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ',
hardwareDetails: {
manufacturer: 'ledger',
deviceId: 'device-xyz',
deviceName: 'My Ledger',
accountIndex: 0,
},
name: 'Cold Wallet',
}

describe('AccountInfoCard', () => {
beforeEach(() => {
mockUseAccountInformationQuery.mockReturnValue({
Expand All @@ -184,8 +199,21 @@ describe('AccountInfoCard', () => {
],
hasMultipleHDWallets: false,
})
mockUseLedgerDeviceGroups.mockReturnValue({
ledgerDeviceGroups: [
{
deviceId: 'device-xyz',
deviceName: 'My Ledger',
accounts: [ledgerAccount],
firstAccount: ledgerAccount,
accountCount: 1,
},
],
hasMultipleLedgerDevices: false,
})
mockUseAccountLogicalType.mockImplementation((address: string) => {
if (address === hdAccount.address) return 'HdKey'
if (address === ledgerAccount.address) return 'LedgerBle'
if (address === watchAccount.address) return 'NoAuth'
return null
})
Expand Down Expand Up @@ -306,4 +334,38 @@ describe('AccountInfoCard', () => {

expect(screen.queryByText('min_balance_info.description')).toBeNull()
})

it('shows wallet structure toggle for Ledger accounts', () => {
render(
<AccountInfoCard
account={ledgerAccount}
onClose={vi.fn()}
/>,
)
expect(
screen.getByText('account_info.see_wallet_structure'),
).toBeTruthy()
})

it('renders Ledger device name in expanded structure', () => {
render(
<AccountInfoCard
account={ledgerAccount}
onClose={vi.fn()}
/>,
)
fireEvent.click(screen.getByText('account_info.see_wallet_structure'))
expect(screen.getByTestId('expandable-panel')).toBeTruthy()
expect(screen.getByText('My Ledger')).toBeTruthy()
})

it('renders account type label for Ledger account', () => {
render(
<AccountInfoCard
account={ledgerAccount}
onClose={vi.fn()}
/>,
)
expect(screen.getByText('account_info.type_ledger')).toBeTruthy()
})
})
Loading
Loading