Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type PWTouchableOpacityProps,
PWView,
} from '@components/core'
import { dedupeSecondaryLabel } from '@perawallet/wallet-core-shared'
import { CopyableText } from '@components/CopyableText'
import { ContactAvatar } from '@components/ContactAvatar'
import type { ContactAvatarVariant } from '@components/ContactAvatar/styles'
Expand Down Expand Up @@ -140,7 +141,9 @@ export const AddressDisplay = ({
/>
)
} else if (contact) {
const showSecondary = contact.name !== truncatedAddress
const showSecondary = Boolean(
dedupeSecondaryLabel(contact.name, truncatedAddress),
)
const primaryText = (
<PWText
variant={textProps?.variant ?? 'bodyLarge'}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import { useMemo } from 'react'
import {
LONG_ADDRESS_LENGTH,
type Optional,
truncateAlgorandAddress,
} from '@perawallet/wallet-core-shared'
Expand All @@ -28,8 +29,6 @@ import type { IconName } from '@components/core'
export type AddressFormat = 'short' | 'long' | 'full'
export type AddressDisplayType = 'full' | 'simple' | 'address-only'

const LONG_ADDRESS_FORMAT = 20

type UseAddressDisplayProps = {
address: string
addressFormat: AddressFormat
Expand Down Expand Up @@ -91,7 +90,7 @@ const formatAddress = (address: string, format: AddressFormat): string => {
return address
}
case 'long': {
return truncateAlgorandAddress(address, LONG_ADDRESS_FORMAT)
return truncateAlgorandAddress(address, LONG_ADDRESS_LENGTH)
}
case 'short':
default: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
*/

import { getAccountDisplayName } from '@perawallet/wallet-core-accounts'
import { dedupeSecondaryLabel } from '@perawallet/wallet-core-shared'
import { PWListItemLayout, PWText, PWView } from '@components/core'
import { ContactAvatar } from '@components/ContactAvatar'
import { AccountIcon } from '@modules/accounts/components/AccountIcon'
Expand Down Expand Up @@ -65,8 +66,7 @@ export const AddressListItem = ({
: (contact?.name ?? nfdName ?? truncatedAddress)
// Show the address underneath only when the primary line is a distinct
// label (name/NFD); otherwise the primary line already *is* the address.
const secondary =
primary === truncatedAddress ? undefined : truncatedAddress
const secondary = dedupeSecondaryLabel(primary, truncatedAddress)

const avatar = account ? (
<AccountIcon
Expand Down
2 changes: 0 additions & 2 deletions apps/mobile/src/constants/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export const OFFLINE_BANNER_FADE_MS = 200
export const SHORT_PROMPT_DISPLAY_DELAY = 300
export const LONG_PROMPT_DISPLAY_DELAY = 3000

export const LONG_ADDRESS_FORMAT = 20

export const SEARCH_DEBOUNCE_TIME = 400
export const SEARCH_DEBOUNCE_TIME_SHORT = 75
export const ASSET_LIST_ITEM_MIN_HEIGHT = 64
Expand Down
8 changes: 6 additions & 2 deletions apps/mobile/src/hooks/__tests__/useResolvedAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ vi.mock('@perawallet/wallet-core-nfd', () => ({
}))

vi.mock('@perawallet/wallet-core-shared', () => ({
truncateAlgorandAddress: (address: string, length?: number) => {
const half = Math.floor((length ?? 6) / 2)
// Defaults must mirror the real constants so the mock reproduces the
// production 5…5 / 10…10 forms.
SHORT_ADDRESS_LENGTH: 11,
LONG_ADDRESS_LENGTH: 20,
truncateAlgorandAddress: (address: string, maxLength = 11) => {
const half = Math.floor(maxLength / 2)
return `${address.slice(0, half)}...${address.slice(-half)}`
},
}))
Expand Down
7 changes: 4 additions & 3 deletions apps/mobile/src/hooks/useResolvedAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
limitations under the License
*/

import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'
import {
LONG_ADDRESS_LENGTH,
truncateAlgorandAddress,
} from '@perawallet/wallet-core-shared'
import { useNfdForAddressQuery } from '@perawallet/wallet-core-nfd'
import { useMemo } from 'react'

const LONG_ADDRESS_LENGTH = 20

export type AddressFormat = 'short' | 'long' | 'full'

type UseResolvedAddressResult = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
*/

import { useTheme } from '@rneui/themed'
import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'
import {
getAccountDisplayName,
type WalletAccount,
} from '@perawallet/wallet-core-accounts'
import { type WalletAccount } from '@perawallet/wallet-core-accounts'
import {
PWIcon,
type PWIconProps,
Expand All @@ -27,11 +23,9 @@ import {
} from '@components/core'
import peraCardImage from '@assets/images/pera-card.png'
import { useStyles } from './styles'
import { useAccountDisplay } from './useAccountDisplay'

import { AccountIcon, type AccountIconProps } from '../AccountIcon'
import { useMemo } from 'react'
import { useNfdForAddressQuery } from '@perawallet/wallet-core-nfd'
import { useAccountTypeLabel } from '@modules/accounts/hooks/useAccountTypeLabel'

/** Pera Card identity rendered in place of an account (header trigger). */
export type AccountDisplayCard = {
Expand Down Expand Up @@ -75,22 +69,8 @@ export const AccountDisplay = ({
}: AccountDisplayProps) => {
const { theme } = useTheme()
const styles = useStyles({ noBorder })
const displayName = useMemo(
() => (account ? getAccountDisplayName(account) : 'No Account'),
[account],
)
const address = useMemo(
() => (account ? truncateAlgorandAddress(account?.address) : undefined),
[account],
)

const { data: nfdNames } = useNfdForAddressQuery(account?.address ?? '', {
enabled: !!account?.address,
})

const nfdName = useMemo(() => nfdNames?.at(0)?.name, [nfdNames])

const { label: accountTypeLabel } = useAccountTypeLabel(account)
const { displayName, secondaryText, renderSecondary, showTypeAsSecondary } =
useAccountDisplay({ account, compact, showAccountType })

if (card) {
return (
Expand Down Expand Up @@ -131,14 +111,6 @@ export const AccountDisplay = ({
)
}

const hasName = Boolean(nfdName) || displayName !== address
const showTypeAsSecondary =
showAccountType && !hasName && Boolean(accountTypeLabel)
const renderSecondary = hasName || showTypeAsSecondary
const secondaryText = showTypeAsSecondary
? accountTypeLabel
: (nfdName ?? address)

return (
<PWView
{...rest}
Expand All @@ -164,7 +136,7 @@ export const AccountDisplay = ({
{displayName}
</PWText>
)}
{(compact || renderSecondary) && (
{renderSecondary && (
<PWText
style={styles.addressText}
variant='body'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
Copyright 2022-2026 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 } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'

import { useAccountDisplay } from '../useAccountDisplay'

import type { WalletAccount } from '@perawallet/wallet-core-accounts'

let mockNfdNames: { name: string }[] | undefined
let mockAccountTypeLabel: string | undefined

// The global setup stubs getAccountDisplayName as `name || ''`, which erases
// the unnamed-account (truncated-address) behavior this hook branches on.
vi.mock('@perawallet/wallet-core-accounts', async importOriginal =>
importOriginal<typeof import('@perawallet/wallet-core-accounts')>(),
)

vi.mock('@perawallet/wallet-core-nfd', () => ({
useNfdForAddressQuery: () => ({ data: mockNfdNames }),
}))

vi.mock('@modules/accounts/hooks/useAccountTypeLabel', () => ({
useAccountTypeLabel: () => ({ label: mockAccountTypeLabel }),
}))

const ADDRESS = 'A'.repeat(58)

const makeAccount = (name?: string): WalletAccount =>
({ address: ADDRESS, name }) as unknown as WalletAccount

describe('useAccountDisplay', () => {
beforeEach(() => {
mockNfdNames = undefined
mockAccountTypeLabel = undefined
})

it('shows the truncated address as secondary for a named account', () => {
const { result } = renderHook(() =>
useAccountDisplay({
account: makeAccount('My Wallet'),
compact: false,
showAccountType: false,
}),
)

expect(result.current.displayName).toBe('My Wallet')
expect(result.current.secondaryText).toBe(
truncateAlgorandAddress(ADDRESS),
)
expect(result.current.renderSecondary).toBe(true)
})

it('suppresses the secondary line when it repeats the primary', () => {
mockNfdNames = [{ name: 'pera.algo' }]

const { result } = renderHook(() =>
useAccountDisplay({
account: makeAccount('pera.algo'),
compact: false,
showAccountType: false,
}),
)

expect(result.current.displayName).toBe('pera.algo')
expect(result.current.renderSecondary).toBe(false)
})

it('keeps the raw secondary in compact mode where it is the only label', () => {
mockNfdNames = [{ name: 'pera.algo' }]

const { result } = renderHook(() =>
useAccountDisplay({
account: makeAccount('pera.algo'),
compact: true,
showAccountType: false,
}),
)

expect(result.current.secondaryText).toBe('pera.algo')
expect(result.current.renderSecondary).toBe(true)
})

it('shows the account type for unnamed accounts when requested', () => {
mockAccountTypeLabel = 'Ledger account'

const { result } = renderHook(() =>
useAccountDisplay({
account: makeAccount(),
compact: false,
showAccountType: true,
}),
)

expect(result.current.secondaryText).toBe('Ledger account')
expect(result.current.showTypeAsSecondary).toBe(true)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
Copyright 2022-2026 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 { useMemo } from 'react'
import {
dedupeSecondaryLabel,
truncateAlgorandAddress,
type Optional,
} from '@perawallet/wallet-core-shared'
import {
getAccountDisplayName,
type WalletAccount,
} from '@perawallet/wallet-core-accounts'
import { useNfdForAddressQuery } from '@perawallet/wallet-core-nfd'
import { useAccountTypeLabel } from '@modules/accounts/hooks/useAccountTypeLabel'

type UseAccountDisplayParams = {
account: Optional<WalletAccount>
compact: boolean
showAccountType: boolean
}

type UseAccountDisplayResult = {
displayName: string
secondaryText: Optional<string>
renderSecondary: boolean
showTypeAsSecondary: boolean
}

/** Derives the primary/secondary labels an account renders with. */
export const useAccountDisplay = ({
account,
compact,
showAccountType,
}: UseAccountDisplayParams): UseAccountDisplayResult => {
const displayName = useMemo(
() => (account ? getAccountDisplayName(account) : 'No Account'),
[account],
)
const address = useMemo(
() => (account ? truncateAlgorandAddress(account?.address) : undefined),
[account],
)

const { data: nfdNames } = useNfdForAddressQuery(account?.address ?? '', {
enabled: !!account?.address,
})

const nfdName = useMemo(() => nfdNames?.at(0)?.name, [nfdNames])

const { label: accountTypeLabel } = useAccountTypeLabel(account)

const hasName = Boolean(nfdName) || displayName !== address
const showTypeAsSecondary =
showAccountType && !hasName && Boolean(accountTypeLabel)
const rawSecondary = showTypeAsSecondary
? accountTypeLabel
: (nfdName ?? address)
// Suppress the secondary line when it would just repeat the primary name
// (e.g. a custom name identical to its NFD). Not in `compact`, where the
// primary is hidden and the secondary stands alone as the only label.
const dedupedSecondary = dedupeSecondaryLabel(displayName, rawSecondary)
const secondaryText = compact ? rawSecondary : dedupedSecondary
const renderSecondary = compact || Boolean(dedupedSecondary)

return { displayName, secondaryText, renderSecondary, showTypeAsSecondary }
}
Loading
Loading