From 93ebfa9e7a30f4408b4b6cc3113fe7b0df693373 Mon Sep 17 00:00:00 2001 From: j0ntz Date: Fri, 13 Mar 2026 11:50:03 -0700 Subject: [PATCH 1/2] Fix contacts permission modal warnings Clean up the existing contacts permission modal warnings so the feature work can build on a warning-free file. --- eslint.config.mjs | 2 +- src/components/modals/ContactsPermissionModal.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 62fe497f70b..5d9647a5197 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -175,7 +175,7 @@ export default [ 'src/components/modals/CategoryModal.tsx', 'src/components/modals/ContactListModal.tsx', - 'src/components/modals/ContactsPermissionModal.tsx', + 'src/components/modals/CountryListModal.tsx', 'src/components/modals/DateModal.tsx', 'src/components/modals/FiatListModal.tsx', diff --git a/src/components/modals/ContactsPermissionModal.tsx b/src/components/modals/ContactsPermissionModal.tsx index 4e83c93ad70..6a3a1227f86 100644 --- a/src/components/modals/ContactsPermissionModal.tsx +++ b/src/components/modals/ContactsPermissionModal.tsx @@ -47,8 +47,9 @@ export function maybeShowContactsPermissionModal(): ThunkAction< // Bail if we already have permission: const contactsPermissionOn = - (await check(permissionNames.contacts).catch(_error => 'denied')) === - 'granted' + (await check(permissionNames.contacts).catch( + (_error: unknown) => 'denied' + )) === 'granted' if (contactsPermissionOn) return // Show the modal: @@ -65,7 +66,7 @@ export function maybeShowContactsPermissionModal(): ThunkAction< * Shows the modal if it hasn't been shown before, and attempts to set the * system contacts permission setting */ -function ContactsPermissionModal(props: Props) { +const ContactsPermissionModal: React.FC = props => { const { bridge } = props const theme = useTheme() const styles = getStyles(theme) From 43bea65cc3b8e2299e48f7b004f82688c7286428 Mon Sep 17 00:00:00 2001 From: j0ntz Date: Fri, 13 Mar 2026 14:42:46 -0700 Subject: [PATCH 2/2] Fix contacts permission prompt timing Prevent contact permission prompts from appearing during passive thumbnail lookups and limit them to transaction-list or explicit payee edit flows. --- CHANGELOG.md | 1 + eslint.config.mjs | 4 +-- src/components/modals/ContactListModal.tsx | 12 +++---- .../modals/ContactsPermissionModal.tsx | 10 ++++++ .../scenes/TransactionListScene.tsx | 17 ++++++++++ src/hooks/redux/useContactThumbnail.ts | 32 ++----------------- 6 files changed, 35 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0471b1ba8f9..c3b92073468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - added: Debug settings scene (Developer Mode only) with nodes/servers inspection, engine `dataDump` viewer, and log viewer - fixed: Swap quote timeout error interrupting user after cancelling a slow swap search - fixed: Disable "Migrate Wallets" button when no assets are available to migrate +- fixed: Contacts permission prompt no longer appears on first receive and only shows from transaction-list or payee edit flows ## 4.45.0 (2025-03-10) diff --git a/eslint.config.mjs b/eslint.config.mjs index 5d9647a5197..c3f88571c8f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -174,8 +174,6 @@ export default [ 'src/components/modals/CategoryModal.tsx', - 'src/components/modals/ContactListModal.tsx', - 'src/components/modals/CountryListModal.tsx', 'src/components/modals/DateModal.tsx', 'src/components/modals/FiatListModal.tsx', @@ -405,7 +403,7 @@ export default [ 'src/controllers/loan-manager/redux/actions.ts', 'src/controllers/loan-manager/util/waitForLoanAccountSync.ts', 'src/hooks/animations/useFadeAnimation.ts', - 'src/hooks/redux/useContactThumbnail.ts', + 'src/hooks/useAbortable.ts', 'src/hooks/useAccountSyncRatio.tsx', 'src/hooks/useAsyncEffect.ts', diff --git a/src/components/modals/ContactListModal.tsx b/src/components/modals/ContactListModal.tsx index ec581aa97e6..1025742c895 100644 --- a/src/components/modals/ContactListModal.tsx +++ b/src/components/modals/ContactListModal.tsx @@ -9,10 +9,9 @@ import { lstrings } from '../../locales/strings' import { useDispatch, useSelector } from '../../types/reactRedux' import type { GuiContact } from '../../types/types' import { normalizeForSearch } from '../../util/utils' -import { requestContactsPermission } from '../services/PermissionsManager' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { SelectableRow } from '../themed/SelectableRow' -import { maybeShowContactsPermissionModal } from './ContactsPermissionModal' +import { promptForContactsPermission } from './ContactsPermissionModal' import { ListModal } from './ListModal' export interface ContactModalResult { @@ -26,11 +25,11 @@ interface Props { contactName: string } -export function ContactListModal({ +export const ContactListModal: React.FC = ({ bridge, contactType, contactName -}: Props): React.ReactElement { +}) => { const theme = useTheme() const styles = getStyles(theme) const contacts = useSelector(state => state.contacts) @@ -96,10 +95,7 @@ export function ContactListModal({ useAsyncEffect( async () => { - const result = await dispatch(maybeShowContactsPermissionModal()) - if (result === 'allow') { - await requestContactsPermission(true) - } + await dispatch(promptForContactsPermission()) }, [], 'ContactListModal' diff --git a/src/components/modals/ContactsPermissionModal.tsx b/src/components/modals/ContactsPermissionModal.tsx index 6a3a1227f86..9cfb38f4da2 100644 --- a/src/components/modals/ContactsPermissionModal.tsx +++ b/src/components/modals/ContactsPermissionModal.tsx @@ -12,6 +12,7 @@ import { config } from '../../theme/appConfig' import type { ThunkAction } from '../../types/reduxTypes' import { ButtonsModal } from '../modals/ButtonsModal' import { Airship } from '../services/AirshipInstance' +import { requestContactsPermission } from '../services/PermissionsManager' import { cacheStyles, type Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' @@ -62,6 +63,15 @@ export function maybeShowContactsPermissionModal(): ThunkAction< } } +export function promptForContactsPermission(): ThunkAction> { + return async dispatch => { + const result = await dispatch(maybeShowContactsPermissionModal()) + if (result === 'allow') { + await requestContactsPermission(true) + } + } +} + /** * Shows the modal if it hasn't been shown before, and attempts to set the * system contacts permission setting diff --git a/src/components/scenes/TransactionListScene.tsx b/src/components/scenes/TransactionListScene.tsx index 4b3d9143702..e3c94c47a8b 100644 --- a/src/components/scenes/TransactionListScene.tsx +++ b/src/components/scenes/TransactionListScene.tsx @@ -36,6 +36,7 @@ import { getWalletName } from '../../util/CurrencyWalletHelpers' import { calculateSpamThreshold, unixToLocaleDateTime } from '../../util/utils' import { SceneWrapper } from '../common/SceneWrapper' import { withWallet } from '../hoc/withWallet' +import { promptForContactsPermission } from '../modals/ContactsPermissionModal' import { HeaderTitle } from '../navigation/HeaderTitle' import { cacheStyles, useTheme } from '../services/ThemeContext' import { ExplorerCard } from '../themed/ExplorerCard' @@ -131,6 +132,13 @@ const TransactionListComponent: React.FC = props => { return out }, [atEnd, isTransactionListUnsupported, transactions]) + const hasNamedTransactions = React.useMemo(() => { + return transactions.some(transaction => { + const metadataName = transaction.metadata?.name + return metadataName != null && metadataName.trim() !== '' + }) + }, [transactions]) + // --------------------------------------------------------------------------- // Side-Effects // --------------------------------------------------------------------------- @@ -142,6 +150,15 @@ const TransactionListComponent: React.FC = props => { } }, [enabledTokenIds, navigation, tokenId]) + useAsyncEffect( + async () => { + if (!hasNamedTransactions) return + await dispatch(promptForContactsPermission()) + }, + [hasNamedTransactions], + 'TransactionListScene contacts permission' + ) + // Automatically navigate to the token activation confirmation scene if // the token appears in the unactivatedTokenIds list once the wallet loads // this state. diff --git a/src/hooks/redux/useContactThumbnail.ts b/src/hooks/redux/useContactThumbnail.ts index 8fc10977a26..30c8152426d 100644 --- a/src/hooks/redux/useContactThumbnail.ts +++ b/src/hooks/redux/useContactThumbnail.ts @@ -1,42 +1,14 @@ import * as React from 'react' -import { check } from 'react-native-permissions' -import { maybeShowContactsPermissionModal } from '../../components/modals/ContactsPermissionModal' -import { requestContactsPermission } from '../../components/services/PermissionsManager' import { MERCHANT_CONTACTS } from '../../constants/MerchantContacts' -import { permissionNames } from '../../reducers/PermissionsReducer' -import { useDispatch, useSelector } from '../../types/reactRedux' +import { useSelector } from '../../types/reactRedux' import { normalizeForSearch } from '../../util/utils' -import { useAsyncEffect } from '../useAsyncEffect' /** - * Looks up a thumbnail image for a contact. Will show a contacts permission - * request modal if we haven't shown it before and the system contacts - * permission is not granted. + * Looks up a thumbnail image for a contact using existing contacts data. */ export const useContactThumbnail = (name?: string): string | undefined => { const contacts = useSelector(state => state.contacts) - const dispatch = useDispatch() - - useAsyncEffect( - async () => { - const contactsPermission = await check(permissionNames.contacts).catch( - (_error: unknown) => 'denied' - ) - - if ( - contactsPermission !== 'granted' && - contactsPermission !== 'limited' - ) { - const result = await dispatch(maybeShowContactsPermissionModal()) - if (result === 'allow') { - await requestContactsPermission(true) - } - } - }, - [], - 'useContactThumbnail' - ) return React.useMemo(() => { if (name == null) return