Skip to content
Merged
10 changes: 10 additions & 0 deletions __mocks__/react-native-nitro-modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const NitroModules = {
Comment thread
Eskalifer1 marked this conversation as resolved.
createHybridObject: jest.fn(() => ({
getAll: jest.fn(() => Promise.resolve([])),
})),
};

export {
// eslint-disable-next-line import/prefer-default-export
NitroModules,
};
53 changes: 53 additions & 0 deletions src/hooks/useContactImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {useCallback, useState} from 'react';
import {RESULTS} from 'react-native-permissions';
import type {PermissionStatus} from 'react-native-permissions';
import contactImport from '@libs/ContactImport';
import type {ContactImportResult} from '@libs/ContactImport/types';
import useContactPermissions from '@libs/ContactPermission/useContactPermissions';
import getContacts from '@libs/ContactUtils';
import type {SearchOption} from '@libs/OptionsListUtils';
import type {PersonalDetails} from '@src/types/onyx';

/**
* Return type of the useContactImport hook.
*/
type UseContactImportResult = {
contacts: Array<SearchOption<PersonalDetails>>;
contactPermissionState: PermissionStatus;
importAndSaveContacts: () => void;
setContactPermissionState: React.Dispatch<React.SetStateAction<PermissionStatus>>;
};

/**
* Custom hook that handles importing device contacts,
* managing permissions, and transforming contact data
* into a format suitable for use in the app.
*/
function useContactImport(): UseContactImportResult {
const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE);
const [contacts, setContacts] = useState<Array<SearchOption<PersonalDetails>>>([]);

const importAndSaveContacts = useCallback(() => {
contactImport().then(({contactList, permissionStatus}: ContactImportResult) => {
setContactPermissionState(permissionStatus);
const usersFromContact = getContacts(contactList);
setContacts(usersFromContact);
});
}, []);

useContactPermissions({
importAndSaveContacts,
setContacts,
contactPermissionState,
setContactPermissionState,
});

return {
contacts,
contactPermissionState,
importAndSaveContacts,
setContactPermissionState,
};
}

export default useContactImport;
6 changes: 4 additions & 2 deletions src/pages/NewChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import SelectCircle from '@components/SelectCircle';
import SelectionList from '@components/SelectionList';
import type {ListItem, SelectionListHandle} from '@components/SelectionList/types';
import UserListItem from '@components/SelectionList/UserListItem';
import useContactImport from '@hooks/useContactImport';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useDebouncedState from '@hooks/useDebouncedState';
import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
Expand Down Expand Up @@ -60,6 +61,7 @@ function useOptions() {
const [newGroupDraft] = useOnyx(ONYXKEYS.NEW_GROUP_CHAT_DRAFT, {canBeMissing: true});
const personalData = useCurrentUserPersonalDetails();
const {didScreenTransitionEnd} = useScreenWrapperTransitionStatus();
const {contacts} = useContactImport();
const {options: listOptions, areOptionsInitialized} = useOptionsList({
shouldInitialize: didScreenTransitionEnd,
});
Expand All @@ -68,15 +70,15 @@ function useOptions() {
const filteredOptions = memoizedGetValidOptions(
{
reports: listOptions.reports ?? [],
personalDetails: listOptions.personalDetails ?? [],
personalDetails: (listOptions.personalDetails ?? []).concat(contacts),
},
{
betas: betas ?? [],
includeSelfDM: true,
},
);
return filteredOptions;
}, [betas, listOptions.personalDetails, listOptions.reports]);
}, [betas, listOptions.personalDetails, listOptions.reports, contacts]);

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

Expand Down
28 changes: 4 additions & 24 deletions src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import type {Ref} from 'react';
import type {GestureResponderEvent} from 'react-native';
import {InteractionManager} from 'react-native';
import {RESULTS} from 'react-native-permissions';
import type {PermissionStatus} from 'react-native-permissions';
import Button from '@components/Button';
import ContactPermissionModal from '@components/ContactPermissionModal';
import EmptySelectionListContent from '@components/EmptySelectionListContent';
Expand All @@ -19,6 +18,7 @@ import ReferralProgramCTA from '@components/ReferralProgramCTA';
import SelectionList from '@components/SelectionList';
import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem';
import type {SelectionListHandle} from '@components/SelectionList/types';
import useContactImport from '@hooks/useContactImport';
import useDebouncedState from '@hooks/useDebouncedState';
import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -27,17 +27,13 @@ import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import useScreenWrapperTransitionStatus from '@hooks/useScreenWrapperTransitionStatus';
import useThemeStyles from '@hooks/useThemeStyles';
import contactImport from '@libs/ContactImport';
import type {ContactImportResult} from '@libs/ContactImport/types';
import useContactPermissions from '@libs/ContactPermission/useContactPermissions';
import getContacts from '@libs/ContactUtils';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import getPlatform from '@libs/getPlatform';
import goToSettings from '@libs/goToSettings';
import {isMovingTransactionFromTrackExpense} from '@libs/IOUUtils';
import memoize from '@libs/memoize';
import Navigation from '@libs/Navigation/Navigation';
import type {Option, SearchOption, Section} from '@libs/OptionsListUtils';
import type {Option, Section} from '@libs/OptionsListUtils';
import {
filterAndOrderOptions,
formatSectionsFromSearchTerm,
Expand All @@ -59,7 +55,6 @@ import type {IOUAction, IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PersonalDetails} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/IOU';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import ImportContactButton from './ImportContactButton';
Expand Down Expand Up @@ -105,7 +100,7 @@ function MoneyRequestParticipantsSelector(
const {translate} = useLocalize();
const styles = useThemeStyles();
const [betas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true});
const [contactPermissionState, setContactPermissionState] = useState<PermissionStatus>(RESULTS.UNAVAILABLE);
const {contactPermissionState, contacts, setContactPermissionState, importAndSaveContacts} = useContactImport();
const platform = getPlatform();
const isNative = platform === CONST.PLATFORM.ANDROID || platform === CONST.PLATFORM.IOS;
const showImportContacts = isNative && !(contactPermissionState === RESULTS.GRANTED || contactPermissionState === RESULTS.LIMITED);
Expand All @@ -123,7 +118,7 @@ function MoneyRequestParticipantsSelector(
shouldInitialize: didScreenTransitionEnd,
});
const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: (val) => val?.reports});
const [contacts, setContacts] = useState<Array<SearchOption<PersonalDetails>>>([]);

const [textInputAutoFocus, setTextInputAutoFocus] = useState<boolean>(!isNative);
const selectionListRef = useRef<SelectionListHandle | null>(null);
const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]);
Expand All @@ -136,14 +131,6 @@ function MoneyRequestParticipantsSelector(
const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp;
const canShowManagerMcTest = useMemo(() => !hasBeenAddedToNudgeMigration && action !== CONST.IOU.ACTION.SUBMIT, [hasBeenAddedToNudgeMigration, action]);

const importAndSaveContacts = useCallback(() => {
contactImport().then(({contactList, permissionStatus}: ContactImportResult) => {
setContactPermissionState(permissionStatus);
const usersFromContact = getContacts(contactList);
setContacts(usersFromContact);
});
}, []);

useEffect(() => {
searchInServer(debouncedSearchTerm.trim());
}, [debouncedSearchTerm]);
Expand All @@ -154,13 +141,6 @@ function MoneyRequestParticipantsSelector(
initializeOptions();
}, [initializeOptions]);

useContactPermissions({
importAndSaveContacts,
setContacts,
contactPermissionState,
setContactPermissionState,
});

const defaultOptions = useMemo(() => {
if (!areOptionsInitialized || !didScreenTransitionEnd) {
return {
Expand Down
21 changes: 21 additions & 0 deletions tests/ui/NewChatPageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@ import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'

jest.mock('@react-navigation/native');
jest.mock('@src/libs/Navigation/navigationRef');
jest.mock('react-native-permissions', () => ({
// eslint-disable-next-line @typescript-eslint/naming-convention
__esModule: true,
RESULTS: {
UNAVAILABLE: 'unavailable',
GRANTED: 'granted',
LIMITED: 'limited',
DENIED: 'denied',
BLOCKED: 'blocked',
},
check: jest.fn(() => Promise.resolve('unavailable')),
request: jest.fn(() => Promise.resolve('unavailable')),
PERMISSIONS: {
IOS: {
CONTACTS: 'ios.permission.CONTACTS',
},
ANDROID: {
READ_CONTACTS: 'android.permission.READ_CONTACTS',
},
},
}));

const wrapper = ({children}: {children: React.ReactNode}) => (
<OnyxListItemProvider>
Expand Down