Skip to content

Commit aa807db

Browse files
Merge pull request Expensify#66910 from Eskalifer1/fix/65916
Fix/65916 Added imported contacts to new chat page
2 parents 85fdcf3 + 6208bd6 commit aa807db

5 files changed

Lines changed: 92 additions & 26 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const NitroModules = {
2+
createHybridObject: jest.fn(() => ({
3+
getAll: jest.fn(() => Promise.resolve([])),
4+
})),
5+
};
6+
7+
export {
8+
// eslint-disable-next-line import/prefer-default-export
9+
NitroModules,
10+
};

src/hooks/useContactImport.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {useCallback, useState} from 'react';
2+
import {RESULTS} from 'react-native-permissions';
3+
import type {PermissionStatus} from 'react-native-permissions';
4+
import contactImport from '@libs/ContactImport';
5+
import type {ContactImportResult} from '@libs/ContactImport/types';
6+
import useContactPermissions from '@libs/ContactPermission/useContactPermissions';
7+
import getContacts from '@libs/ContactUtils';
8+
import type {SearchOption} from '@libs/OptionsListUtils';
9+
import type {PersonalDetails} from '@src/types/onyx';
10+
11+
/**
12+
* Return type of the useContactImport hook.
13+
*/
14+
type UseContactImportResult = {
15+
contacts: Array<SearchOption<PersonalDetails>>;
16+
contactPermissionState: PermissionStatus;
17+
importAndSaveContacts: () => void;
18+
setContactPermissionState: React.Dispatch<React.SetStateAction<PermissionStatus>>;
19+
};
20+
21+
/**
22+
* Custom hook that handles importing device contacts,
23+
* managing permissions, and transforming contact data
24+
* into a format suitable for use in the app.
25+
*/
26+
function useContactImport(): UseContactImportResult {
27+
const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE);
28+
const [contacts, setContacts] = useState<Array<SearchOption<PersonalDetails>>>([]);
29+
30+
const importAndSaveContacts = useCallback(() => {
31+
contactImport().then(({contactList, permissionStatus}: ContactImportResult) => {
32+
setContactPermissionState(permissionStatus);
33+
const usersFromContact = getContacts(contactList);
34+
setContacts(usersFromContact);
35+
});
36+
}, []);
37+
38+
useContactPermissions({
39+
importAndSaveContacts,
40+
setContacts,
41+
contactPermissionState,
42+
setContactPermissionState,
43+
});
44+
45+
return {
46+
contacts,
47+
contactPermissionState,
48+
importAndSaveContacts,
49+
setContactPermissionState,
50+
};
51+
}
52+
53+
export default useContactImport;

src/pages/NewChatPage.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SelectCircle from '@components/SelectCircle';
1111
import SelectionList from '@components/SelectionList';
1212
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
1313
import UserListItem from '@components/SelectionList/UserListItem';
14+
import useContactImport from '@hooks/useContactImport';
1415
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
1516
import useDebouncedState from '@hooks/useDebouncedState';
1617
import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
@@ -60,6 +61,7 @@ function useOptions() {
6061
const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {canBeMissing: true});
6162
const personalData = useCurrentUserPersonalDetails();
6263
const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus();
64+
const {contacts} = useContactImport();
6365
const {options: listOptions, areOptionsInitialized} = useOptionsList({
6466
shouldInitialize: didScreenTransitionEnd,
6567
});
@@ -68,15 +70,15 @@ function useOptions() {
6870
const filteredOptions = memoizedGetValidOptions(
6971
{
7072
reports: listOptions.reports ?? [],
71-
personalDetails: listOptions.personalDetails ?? [],
73+
personalDetails: (listOptions.personalDetails ?? []).concat(contacts),
7274
},
7375
{
7476
betas: betas ?? [],
7577
includeSelfDM: true,
7678
},
7779
);
7880
return filteredOptions;
79-
}, [betas, listOptions.personalDetails, listOptions.reports]);
81+
}, [betas, listOptions.personalDetails, listOptions.reports, contacts]);
8082

8183
const unselectedOptions = useMemo(() => filterSelectedOptions(defaultOptions, new Set(selectedOptions.map(({accountID}) => accountID))), [defaultOptions, selectedOptions]);
8284

src/pages/iou/request/MoneyRequestParticipantsSelector.tsx

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {Ref} from 'react';
66
import type {GestureResponderEvent} from 'react-native';
77
import {InteractionManager} from 'react-native';
88
import {RESULTS} from 'react-native-permissions';
9-
import type {PermissionStatus} from 'react-native-permissions';
109
import Button from '@components/Button';
1110
import ContactPermissionModal from '@components/ContactPermissionModal';
1211
import EmptySelectionListContent from '@components/EmptySelectionListContent';
@@ -19,6 +18,7 @@ import ReferralProgramCTA from '@components/ReferralProgramCTA';
1918
import SelectionList from '@components/SelectionList';
2019
import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
2120
import type {SelectionListHandle} from '@components/SelectionList/types';
21+
import useContactImport from '@hooks/useContactImport';
2222
import useDebouncedState from '@hooks/useDebouncedState';
2323
import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
2424
import useLocalize from '@hooks/useLocalize';
@@ -27,17 +27,13 @@ import useOnyx from '@hooks/useOnyx';
2727
import usePolicy from '@hooks/usePolicy';
2828
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
2929
import useThemeStyles from '@hooks/useThemeStyles';
30-
import contactImport from '@libs/ContactImport';
31-
import type {ContactImportResult} from '@libs/ContactImport/types';
32-
import useContactPermissions from '@libs/ContactPermission/useContactPermissions';
33-
import getContacts from '@libs/ContactUtils';
3430
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
3531
import getPlatform from '@libs/getPlatform';
3632
import goToSettings from '@libs/goToSettings';
3733
import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils';
3834
import memoize from '@libs/memoize';
3935
import Navigation from '@libs/Navigation/Navigation';
40-
import type {Option, SearchOption, Section} from '@libs/OptionsListUtils';
36+
import type {Option, Section} from '@libs/OptionsListUtils';
4137
import {
4238
filterAndOrderOptions,
4339
formatSectionsFromSearchTerm,
@@ -59,7 +55,6 @@ import type {IOUAction, IOUType} from '@src/CONST';
5955
import CONST from '@src/CONST';
6056
import ONYXKEYS from '@src/ONYXKEYS';
6157
import ROUTES from '@src/ROUTES';
62-
import type {PersonalDetails} from '@src/types/onyx';
6358
import type {Participant} from '@src/types/onyx/IOU';
6459
import {isEmptyObject} from '@src/types/utils/EmptyObject';
6560
import ImportContactButton from './ImportContactButton';
@@ -105,7 +100,7 @@ function MoneyRequestParticipantsSelector(
105100
const {translate} = useLocalize();
106101
const styles = useThemeStyles();
107102
const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
108-
const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE);
103+
const {contactPermissionState, contacts, setContactPermissionState, importAndSaveContacts} = useContactImport();
109104
const platform = getPlatform();
110105
const isNative = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS;
111106
const showImportContacts = isNative && !(contactPermissionState === RESULTS.GRANTED || contactPermissionState === RESULTS.LIMITED);
@@ -123,7 +118,7 @@ function MoneyRequestParticipantsSelector(
123118
shouldInitialize: didScreenTransitionEnd,
124119
});
125120
const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports});
126-
const [contacts, setContacts] = useState<Array<SearchOption<PersonalDetails>>>([]);
121+
127122
const [textInputAutoFocus, setTextInputAutoFocus] = useState<boolean>(!isNative);
128123
const selectionListRef = useRef<SelectionListHandle | null>(null);
129124
const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]);
@@ -136,14 +131,6 @@ function MoneyRequestParticipantsSelector(
136131
const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp;
137132
const canShowManagerMcTest = useMemo(() => !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT, [hasBeenAddedToNudgeMigration, action]);
138133

139-
const importAndSaveContacts = useCallback(() => {
140-
contactImport().then(({contactList, permissionStatus}: ContactImportResult) => {
141-
setContactPermissionState(permissionStatus);
142-
const usersFromContact = getContacts(contactList);
143-
setContacts(usersFromContact);
144-
});
145-
}, []);
146-
147134
useEffect(() => {
148135
searchInServer(debouncedSearchTerm.trim());
149136
}, [debouncedSearchTerm]);
@@ -154,13 +141,6 @@ function MoneyRequestParticipantsSelector(
154141
initializeOptions();
155142
}, [initializeOptions]);
156143

157-
useContactPermissions({
158-
importAndSaveContacts,
159-
setContacts,
160-
contactPermissionState,
161-
setContactPermissionState,
162-
});
163-
164144
const defaultOptions = useMemo(() => {
165145
if (!areOptionsInitialized || !didScreenTransitionEnd) {
166146
return {

tests/ui/NewChatPageTest.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,27 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'
1919

2020
jest.mock('@react-navigation/native');
2121
jest.mock('@src/libs/Navigation/navigationRef');
22+
jest.mock('react-native-permissions', () => ({
23+
// eslint-disable-next-line @typescript-eslint/naming-convention
24+
__esModule: true,
25+
RESULTS: {
26+
UNAVAILABLE: 'unavailable',
27+
GRANTED: 'granted',
28+
LIMITED: 'limited',
29+
DENIED: 'denied',
30+
BLOCKED: 'blocked',
31+
},
32+
check: jest.fn(() => Promise.resolve('unavailable')),
33+
request: jest.fn(() => Promise.resolve('unavailable')),
34+
PERMISSIONS: {
35+
IOS: {
36+
CONTACTS: 'ios.permission.CONTACTS',
37+
},
38+
ANDROID: {
39+
READ_CONTACTS: 'android.permission.READ_CONTACTS',
40+
},
41+
},
42+
}));
2243

2344
const wrapper = ({children}: {children: React.ReactNode}) => (
2445
<OnyxListItemProvider>

0 commit comments

Comments
 (0)