Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
'android.permission.READ_PHONE_NUMBERS',
'android.permission.MANAGE_OWN_CALLS',
],
// Media is selected through Android's system Photo Picker. Block broad media
// and legacy storage permissions even when a transitive native dependency
// contributes them during manifest merging.
blockedPermissions: [
'android.permission.READ_MEDIA_IMAGES',
'android.permission.READ_MEDIA_VIDEO',
'android.permission.READ_MEDIA_VISUAL_USER_SELECTED',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
],
},
web: {
favicon: './assets/favicon.png',
Expand Down
29 changes: 29 additions & 0 deletions plugins/__tests__/android-media-permissions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ConfigContext } from '@expo/config';

import createExpoConfig from '../../app.config';

jest.mock('zod', () => jest.requireActual('zod'));

const blockedMediaPermissions = [
'android.permission.READ_MEDIA_IMAGES',
'android.permission.READ_MEDIA_VIDEO',
'android.permission.READ_MEDIA_VISUAL_USER_SELECTED',
'android.permission.READ_EXTERNAL_STORAGE',
'android.permission.WRITE_EXTERNAL_STORAGE',
];

describe('Android media permissions', () => {
it('blocks broad media access so system pickers remain the only gallery access path', () => {
const config = createExpoConfig({
config: {
name: 'Resgrid IC',
slug: 'resgrid-ic',
},
} as ConfigContext);

expect(config.android?.blockedPermissions).toEqual(expect.arrayContaining(blockedMediaPermissions));
blockedMediaPermissions.forEach((permission) => {
expect(config.android?.permissions).not.toContain(permission);
});
});
});
27 changes: 27 additions & 0 deletions src/components/calls/__tests__/call-image-picker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as ImagePicker from 'expo-image-picker';

import { launchCallImagePicker } from '@/components/calls/call-image-picker';

jest.mock('expo-image-picker', () => ({
launchImageLibraryAsync: jest.fn(),
}));

const mockLaunchImageLibraryAsync = ImagePicker.launchImageLibraryAsync as jest.MockedFunction<typeof ImagePicker.launchImageLibraryAsync>;

describe('launchCallImagePicker', () => {
it('uses the non-legacy system image picker without requesting broad media access', async () => {
const pickerResult: ImagePicker.ImagePickerResult = {
assets: null,
canceled: true,
};
mockLaunchImageLibraryAsync.mockResolvedValue(pickerResult);

await expect(launchCallImagePicker()).resolves.toBe(pickerResult);
expect(mockLaunchImageLibraryAsync).toHaveBeenCalledWith({
mediaTypes: ['images'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Raw string literal used for the media type risks drift from the implementation and obscures intent. Use the shared MediaType constant/enum from the picker module or Expo API, such as mediaTypes: [MediaType.Images].

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/components/calls/__tests__/call-image-picker.test.ts:

Line 21:

Raw string literal used for the media type risks drift from the implementation and obscures intent. Use the shared `MediaType` constant/enum from the picker module or Expo API, such as `mediaTypes: [MediaType.Images]`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

allowsEditing: true,
quality: 0.8,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Inlined magic number for the quality value risks inconsistency between the test and implementation, also occurring in src/components/calls/call-image-picker.ts:7-7. Import and reuse the named constant from launchCallImagePicker, such as quality: CALL_IMAGE_PICKER_QUALITY.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/components/calls/__tests__/call-image-picker.test.ts:

Line 23:

Inlined magic number for the quality value risks inconsistency between the test and implementation, also occurring in `src/components/calls/call-image-picker.ts:7-7`. Import and reuse the named constant from `launchCallImagePicker`, such as `quality: CALL_IMAGE_PICKER_QUALITY`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

legacy: false,
});
});
});
9 changes: 9 additions & 0 deletions src/components/calls/call-image-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as ImagePicker from 'expo-image-picker';

export const launchCallImagePicker = (): Promise<ImagePicker.ImagePickerResult> =>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Missing JSDoc documentation for the Promise-returning function obscures the resolve value shape, rejection conditions, and await requirement. Add a JSDoc block specifying @returns {Promise<ImagePicker.ImagePickerResult>} and detailing rejection conditions like permission denial or user cancellation.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/components/calls/call-image-picker.ts:

Line 3:

Missing JSDoc documentation for the Promise-returning function obscures the resolve value shape, rejection conditions, and await requirement. Add a JSDoc block specifying `@returns {Promise<ImagePicker.ImagePickerResult>}` and detailing rejection conditions like permission denial or user cancellation.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 0.8,
legacy: false,
});
12 changes: 2 additions & 10 deletions src/components/calls/call-images-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useTranslation } from 'react-i18next';
import { Keyboard, Modal, SafeAreaView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { KeyboardStickyView } from 'react-native-keyboard-controller';

import { launchCallImagePicker } from '@/components/calls/call-image-picker';
import { Loading } from '@/components/common/loading';
import ZeroState from '@/components/common/zero-state';
import { useAnalytics } from '@/hooks/use-analytics';
Expand Down Expand Up @@ -111,16 +112,7 @@ const CallImagesModal: React.FC<CallImagesModalProps> = ({ isOpen, onClose, call

const handleImageSelect = async () => {
try {
const permissionResult = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (permissionResult.status !== 'granted') {
alert(t('common.permission_denied'));
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images'],
allowsEditing: true,
quality: 0.8,
});
const result = await launchCallImagePicker();
if (!result.canceled && result.assets && result.assets.length > 0) {
const asset = result.assets[0];
const filename = asset.fileName || `image_${Date.now()}.png`;
Expand Down
Loading