diff --git a/apps/mobile/src/components/AddressDisplay/AddressDisplay.tsx b/apps/mobile/src/components/AddressDisplay/AddressDisplay.tsx
index c2fb823ba..27d0e02bd 100644
--- a/apps/mobile/src/components/AddressDisplay/AddressDisplay.tsx
+++ b/apps/mobile/src/components/AddressDisplay/AddressDisplay.tsx
@@ -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'
@@ -140,7 +141,9 @@ export const AddressDisplay = ({
/>
)
} else if (contact) {
- const showSecondary = contact.name !== truncatedAddress
+ const showSecondary = Boolean(
+ dedupeSecondaryLabel(contact.name, truncatedAddress),
+ )
const primaryText = (
{
return address
}
case 'long': {
- return truncateAlgorandAddress(address, LONG_ADDRESS_FORMAT)
+ return truncateAlgorandAddress(address, LONG_ADDRESS_LENGTH)
}
case 'short':
default: {
diff --git a/apps/mobile/src/components/AddressListItem/AddressListItem.tsx b/apps/mobile/src/components/AddressListItem/AddressListItem.tsx
index 354261d9d..21d742627 100644
--- a/apps/mobile/src/components/AddressListItem/AddressListItem.tsx
+++ b/apps/mobile/src/components/AddressListItem/AddressListItem.tsx
@@ -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'
@@ -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 ? (
({
}))
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)}`
},
}))
diff --git a/apps/mobile/src/hooks/useResolvedAddress.ts b/apps/mobile/src/hooks/useResolvedAddress.ts
index 75cd7a272..78f631104 100644
--- a/apps/mobile/src/hooks/useResolvedAddress.ts
+++ b/apps/mobile/src/hooks/useResolvedAddress.ts
@@ -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 = {
diff --git a/apps/mobile/src/modules/accounts/components/AccountDisplay/AccountDisplay.tsx b/apps/mobile/src/modules/accounts/components/AccountDisplay/AccountDisplay.tsx
index b7f99af9d..b7a3693b3 100644
--- a/apps/mobile/src/modules/accounts/components/AccountDisplay/AccountDisplay.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountDisplay/AccountDisplay.tsx
@@ -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,
@@ -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 = {
@@ -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 (
@@ -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 (
)}
- {(compact || renderSecondary) && (
+ {renderSecondary && (
+ importOriginal(),
+)
+
+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)
+ })
+})
diff --git a/apps/mobile/src/modules/accounts/components/AccountDisplay/useAccountDisplay.ts b/apps/mobile/src/modules/accounts/components/AccountDisplay/useAccountDisplay.ts
new file mode 100644
index 000000000..566b4e546
--- /dev/null
+++ b/apps/mobile/src/modules/accounts/components/AccountDisplay/useAccountDisplay.ts
@@ -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
+ compact: boolean
+ showAccountType: boolean
+}
+
+type UseAccountDisplayResult = {
+ displayName: string
+ secondaryText: Optional
+ 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 }
+}
diff --git a/apps/mobile/src/modules/accounts/components/AccountInfoCard/RekeyedToRow.tsx b/apps/mobile/src/modules/accounts/components/AccountInfoCard/RekeyedToRow.tsx
index f664c1387..5a271ea8c 100644
--- a/apps/mobile/src/modules/accounts/components/AccountInfoCard/RekeyedToRow.tsx
+++ b/apps/mobile/src/modules/accounts/components/AccountInfoCard/RekeyedToRow.tsx
@@ -16,7 +16,10 @@ import {
getAccountDisplayName,
type WalletAccount,
} from '@perawallet/wallet-core-accounts'
-import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'
+import {
+ dedupeSecondaryLabel,
+ truncateAlgorandAddress,
+} from '@perawallet/wallet-core-shared'
import { useClipboard } from '@hooks/useClipboard'
import { AccountIcon } from '../AccountIcon'
import type { useStyles } from './styles'
@@ -41,8 +44,8 @@ export const RekeyedToRow = ({
const { copyToClipboard } = useClipboard()
const address = authAccount?.address ?? authAddress
const truncated = truncateAlgorandAddress(address)
- const hasCustomName = !!authAccount?.name
- const title = hasCustomName ? getAccountDisplayName(authAccount) : truncated
+ const title = authAccount ? getAccountDisplayName(authAccount) : truncated
+ const secondary = dedupeSecondaryLabel(title, truncated)
const handleCopyAddress = useCallback(() => {
void copyToClipboard(address)
@@ -76,13 +79,13 @@ export const RekeyedToRow = ({
>
{title}
- {hasCustomName && (
+ {!!secondary && (
- {truncated}
+ {secondary}
)}
diff --git a/apps/mobile/src/modules/assets/screens/AssetDetailsScreen/AssetDetailsScreen.tsx b/apps/mobile/src/modules/assets/screens/AssetDetailsScreen/AssetDetailsScreen.tsx
index 8791a594f..5744126af 100644
--- a/apps/mobile/src/modules/assets/screens/AssetDetailsScreen/AssetDetailsScreen.tsx
+++ b/apps/mobile/src/modules/assets/screens/AssetDetailsScreen/AssetDetailsScreen.tsx
@@ -15,6 +15,7 @@ import {
getAccountDisplayName,
useSelectedAccount,
} from '@perawallet/wallet-core-accounts'
+import { dedupeSecondaryLabel } from '@perawallet/wallet-core-shared'
import { useResolvedAddress } from '@hooks/useResolvedAddress'
import { useEffect, useState } from 'react'
import { trackEvent, AssetDetailsEvent, AnalyticsMetadataKey } from '@analytics'
@@ -69,6 +70,15 @@ export const AssetDetailsScreen = ({ route }: AssetDetailsScreenProps) => {
}
}, [assetId, isCollectible])
+ // Dedupe compares rendered strings, so both sides must share the same
+ // ('short') truncation — a 'long'-format secondary would never match and
+ // the address would render twice again.
+ const headerPrimary = getAccountDisplayName(account)
+ const headerSecondary = dedupeSecondaryLabel(
+ headerPrimary,
+ accountDisplayName,
+ )
+
useNavigationHeader({
title: (
@@ -76,16 +86,18 @@ export const AssetDetailsScreen = ({ route }: AssetDetailsScreenProps) => {
variant='h4'
truncate
>
- {account?.name || getAccountDisplayName(account)}
-
-
- {accountDisplayName}
+ {headerPrimary}
+ {!!headerSecondary && (
+
+ {headerSecondary}
+
+ )}
),
right: account ? (
diff --git a/apps/mobile/src/modules/signing/components/SigningWarnings/WarningItem.tsx b/apps/mobile/src/modules/signing/components/SigningWarnings/WarningItem.tsx
index 1c154f636..c95969136 100644
--- a/apps/mobile/src/modules/signing/components/SigningWarnings/WarningItem.tsx
+++ b/apps/mobile/src/modules/signing/components/SigningWarnings/WarningItem.tsx
@@ -19,9 +19,9 @@ import { useLanguage } from '@hooks/useLanguage'
import { PWDivider, PWRoundIcon, PWText, PWView } from '@components/core'
import {
formatNumber,
+ LONG_ADDRESS_LENGTH,
truncateAlgorandAddress,
} from '@perawallet/wallet-core-shared'
-import { LONG_ADDRESS_FORMAT } from '@constants/ui'
type WarningItemProps = {
warning: TransactionWarning
@@ -129,7 +129,7 @@ export const WarningItem = ({
{t(messageKey, {
address: truncateAlgorandAddress(
warning.targetAddress,
- LONG_ADDRESS_FORMAT,
+ LONG_ADDRESS_LENGTH,
),
})}
@@ -141,7 +141,7 @@ export const WarningItem = ({
{t('transactions.warning.from_account', {
address: truncateAlgorandAddress(
warning.senderAddress,
- LONG_ADDRESS_FORMAT,
+ LONG_ADDRESS_LENGTH,
),
})}
diff --git a/apps/mobile/src/modules/transactions/components/TransactionWarnings/TransactionWarningsContent.tsx b/apps/mobile/src/modules/transactions/components/TransactionWarnings/TransactionWarningsContent.tsx
index 9552c92f9..2fc4315c1 100644
--- a/apps/mobile/src/modules/transactions/components/TransactionWarnings/TransactionWarningsContent.tsx
+++ b/apps/mobile/src/modules/transactions/components/TransactionWarnings/TransactionWarningsContent.tsx
@@ -21,9 +21,11 @@ import { SheetHeader } from '@modules/bottom-sheet'
import { useStyles } from './styles'
import { useLanguage } from '@hooks/useLanguage'
import type { PeraDisplayableTransaction } from '@perawallet/wallet-core-blockchain'
-import { truncateAlgorandAddress } from '@perawallet/wallet-core-shared'
+import {
+ LONG_ADDRESS_LENGTH,
+ truncateAlgorandAddress,
+} from '@perawallet/wallet-core-shared'
import { useTheme } from '@rneui/themed'
-import { LONG_ADDRESS_FORMAT } from '@constants/ui'
import { useTransactionWarnings } from './useTransactionWarnings'
export type TransactionWarningsContentProps = {
@@ -70,7 +72,7 @@ export const TransactionWarningsContent = ({
{t('transactions.warning.close_warning', {
address: truncateAlgorandAddress(
warning.targetAddress,
- LONG_ADDRESS_FORMAT,
+ LONG_ADDRESS_LENGTH,
),
})}
@@ -106,7 +108,7 @@ export const TransactionWarningsContent = ({
{t('transactions.warning.rekey_warning', {
address: truncateAlgorandAddress(
warning.targetAddress,
- LONG_ADDRESS_FORMAT,
+ LONG_ADDRESS_LENGTH,
),
})}
diff --git a/apps/mobile/vitest.setup.ts b/apps/mobile/vitest.setup.ts
index ae8c79490..c1e237b62 100644
--- a/apps/mobile/vitest.setup.ts
+++ b/apps/mobile/vitest.setup.ts
@@ -2372,6 +2372,10 @@ vi.mock('@perawallet/wallet-core-shared', async () => {
return value
},
truncateAlgorandAddress: vi.fn(a => a),
+ SHORT_ADDRESS_LENGTH: 11,
+ LONG_ADDRESS_LENGTH: 20,
+ dedupeSecondaryLabel: (primary: string, secondary?: string | null) =>
+ secondary && secondary !== primary ? secondary : undefined,
stripUrlScheme: vi.fn(url => url),
// Real implementations — serialized route params round-trip through
// these, so a stub would silently corrupt every public key fixture.
diff --git a/packages/accounts/src/__tests__/utils.test.ts b/packages/accounts/src/__tests__/utils.test.ts
index f684a3674..d23a46cfc 100644
--- a/packages/accounts/src/__tests__/utils.test.ts
+++ b/packages/accounts/src/__tests__/utils.test.ts
@@ -146,6 +146,28 @@ describe('services/accounts/utils - getAccountDisplayName', () => {
expect(getAccountDisplayName(acc2)).toEqual('ABCDE...VWXYZ')
})
+ test('falls back to the truncated address when the name is the full address', () => {
+ const acc = {
+ id: '7',
+ type: 'hdWallet',
+ address: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ name: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ canSign: true,
+ } as any
+ expect(getAccountDisplayName(acc)).toEqual('ABCDE...VWXYZ')
+ })
+
+ test('falls back to the truncated address when the name is the truncated address', () => {
+ const acc = {
+ id: '8',
+ type: 'hdWallet',
+ address: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
+ name: 'ABCDE...VWXYZ',
+ canSign: true,
+ } as any
+ expect(getAccountDisplayName(acc)).toEqual('ABCDE...VWXYZ')
+ })
+
test('returns "No Account" when account is null', () => {
expect(getAccountDisplayName(null)).toEqual('No Account')
})
diff --git a/packages/accounts/src/utils.ts b/packages/accounts/src/utils.ts
index 13b9ecb62..3d323d30c 100644
--- a/packages/accounts/src/utils.ts
+++ b/packages/accounts/src/utils.ts
@@ -31,9 +31,15 @@ import { RekeyTargetNotFoundError } from './errors'
export const getAccountDisplayName = (account: Nullable) => {
if (!account) return 'No Account'
- if (account.name) return account.name
- if (!account.address) return 'No Address Found'
- return truncateAlgorandAddress(account.address)
+ if (!account.address) return account.name || 'No Address Found'
+ const truncated = truncateAlgorandAddress(account.address)
+ // A name that is just the account's own address — full OR truncated — isn't
+ // a distinguishing label. Treat it as unnamed so callers render a single
+ // truncated address instead of the address twice.
+ const isAddressName =
+ account.name === account.address || account.name === truncated
+ if (account.name && !isAddressName) return account.name
+ return truncated
}
export const isHDWalletAccount = (
diff --git a/packages/shared/src/utils/__tests__/strings.test.ts b/packages/shared/src/utils/__tests__/strings.test.ts
index 03454d9d2..57f7ccbd2 100644
--- a/packages/shared/src/utils/__tests__/strings.test.ts
+++ b/packages/shared/src/utils/__tests__/strings.test.ts
@@ -14,6 +14,7 @@ import { describe, test, expect, vi, beforeEach } from 'vitest'
import { Decimal } from 'decimal.js'
import { encodeToBase64, decodeFromBase64 } from '../strings'
import { hexToBytes, bytesToHex, utf8ByteLength } from '../strings'
+import { dedupeSecondaryLabel } from '../strings'
import {
decodeLongString,
formatCurrency,
@@ -26,6 +27,22 @@ import {
} from '../strings'
import { logger } from '../logging'
+describe('utils/strings - dedupeSecondaryLabel', () => {
+ test('hides the secondary when it equals the primary', () => {
+ expect(dedupeSecondaryLabel('main.algo', 'main.algo')).toBeUndefined()
+ })
+
+ test('keeps a distinct secondary', () => {
+ expect(dedupeSecondaryLabel('Main', 'ABCD…WXYZ')).toBe('ABCD…WXYZ')
+ })
+
+ test('hides an empty, null, or undefined secondary', () => {
+ expect(dedupeSecondaryLabel('Main', '')).toBeUndefined()
+ expect(dedupeSecondaryLabel('Main', null)).toBeUndefined()
+ expect(dedupeSecondaryLabel('Main', undefined)).toBeUndefined()
+ })
+})
+
describe('utils/strings - base64 encoding', () => {
test('encodeToBase64 encodes bytes correctly', () => {
const bytes = new Uint8Array([80, 82, 73, 86, 75, 69, 89]) // 'PRIVKEY'
diff --git a/packages/shared/src/utils/addresses.ts b/packages/shared/src/utils/addresses.ts
index d9f84763a..8d7f16761 100644
--- a/packages/shared/src/utils/addresses.ts
+++ b/packages/shared/src/utils/addresses.ts
@@ -10,11 +10,17 @@
limitations under the License
*/
-const MAX_ADDRESS_DISPLAY = 11
+/**
+ * Total characters retained by {@link truncateAlgorandAddress}, split evenly
+ * between prefix and suffix: 11 renders as `5…5`, 20 renders as `10…10`.
+ * Canonical source of truth — import these instead of re-declaring the length.
+ */
+export const SHORT_ADDRESS_LENGTH = 11
+export const LONG_ADDRESS_LENGTH = 20
export const truncateAlgorandAddress = (
address: string,
- maxLength: number = MAX_ADDRESS_DISPLAY,
+ maxLength: number = SHORT_ADDRESS_LENGTH,
) => {
const prefixLength =
maxLength % 2 === 0 ? maxLength / 2 : (maxLength - 1) / 2
diff --git a/packages/shared/src/utils/strings.ts b/packages/shared/src/utils/strings.ts
index e98a5b107..c4356e04b 100644
--- a/packages/shared/src/utils/strings.ts
+++ b/packages/shared/src/utils/strings.ts
@@ -45,6 +45,17 @@ export const bytesToHex = (bytes: Uint8Array): string => {
export const utf8ByteLength = (value: string): number =>
new TextEncoder().encode(value).length
+/**
+ * Drops a secondary/subtitle label when it duplicates the primary, so identity
+ * displays (account/contact/address name + subtitle) never show the same text
+ * twice. Returns `undefined` to signal "hide the secondary line".
+ */
+export const dedupeSecondaryLabel = (
+ primary: string,
+ secondary: string | null | undefined,
+): string | undefined =>
+ secondary && secondary !== primary ? secondary : undefined
+
/**
* Decode a value that may be a JS number, a string-encoded number, or null.
*