fix(mobile): address Apple review onboarding flows

This commit is contained in:
jjbrunton committed 2026-09-01 09:42:48 +01:00
1 parent 6883befff3
commit 7c8d91c89c
20 files changed
+661 -262

No files matched your search

@@ -1,14 +1,19 @@
import { act, renderHook } from '@testing-library/react-native';
import { ActionSheetIOS, Alert, Platform } from 'react-native';
import { ActionSheetIOS, Alert, Linking, Platform } from 'react-native';
import * as FileSystem from 'expo-file-system/legacy';
import * as ImagePicker from 'expo-image-picker';
import { useAvatarPicker } from '@/hooks/useAvatarPicker';
jest.mock('@/lib/mediaApi', () => ({
uploadMedia: jest.fn(),
}));
jest.mock('expo-file-system/legacy', () => ({
getInfoAsync: jest.fn(),
}));
jest.mock('expo-image-picker', () => ({
getCameraPermissionsAsync: jest.fn(),
requestCameraPermissionsAsync: jest.fn(),
requestMediaLibraryPermissionsAsync: jest.fn(),
launchCameraAsync: jest.fn(),
@@ -23,7 +28,10 @@ describe('useAvatarPicker', () => {
Object.defineProperty(Platform, 'OS', { configurable: true, value: 'android' });
jest.spyOn(Alert, 'alert').mockImplementation(() => undefined);
jest.spyOn(ActionSheetIOS, 'showActionSheetWithOptions').mockImplementation(() => undefined);
jest.spyOn(Linking, 'openSettings').mockResolvedValue(undefined);
(ImagePicker.launchImageLibraryAsync as jest.Mock).mockResolvedValue({ canceled: true, assets: [] });
(ImagePicker.getCameraPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'undetermined', canAskAgain: true });
(ImagePicker.requestCameraPermissionsAsync as jest.Mock).mockResolvedValue({ status: 'granted' });
(FileSystem.getInfoAsync as jest.Mock).mockResolvedValue({ exists: true });
});
@@ -64,4 +72,41 @@ describe('useAvatarPicker', () => {
expect(ImagePicker.requestMediaLibraryPermissionsAsync).toHaveBeenCalledTimes(1);
expect(ImagePicker.launchImageLibraryAsync).toHaveBeenCalledTimes(1);
});
it('does not show a custom alert immediately after a camera permission denial', async () => {
const { result } = renderHook(() => useAvatarPicker());
(ImagePicker.requestCameraPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied', canAskAgain: false });
act(() => result.current.showPicker());
const takePhoto = (Alert.alert as jest.Mock).mock.calls[0][2].find(
(option: { text: string }) => option.text === 'Take Photo'
);
await act(async () => takePhoto.onPress());
expect(Alert.alert).toHaveBeenCalledTimes(1);
expect(ImagePicker.launchCameraAsync).not.toHaveBeenCalled();
});
it('offers Settings only for a later camera attempt that iOS cannot prompt again', async () => {
(ImagePicker.getCameraPermissionsAsync as jest.Mock).mockResolvedValueOnce({ status: 'denied', canAskAgain: false });
const { result } = renderHook(() => useAvatarPicker());
act(() => result.current.showPicker());
const takePhoto = (Alert.alert as jest.Mock).mock.calls[0][2].find(
(option: { text: string }) => option.text === 'Take Photo'
);
await act(async () => takePhoto.onPress());
const settingsAlert = (Alert.alert as jest.Mock).mock.calls[1];
expect(settingsAlert[0]).toBe('Camera Access');
expect(settingsAlert[2]).toEqual([
{ text: 'Not Now', style: 'cancel' },
expect.objectContaining({ text: 'Open Settings' }),
]);
await act(async () => settingsAlert[2][1].onPress());
expect(Linking.openSettings).toHaveBeenCalledTimes(1);
expect(ImagePicker.requestCameraPermissionsAsync).not.toHaveBeenCalled();
});
});