Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: [IOAPPX-479] Fix camera permission request on Android devices #6687

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
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
70 changes: 35 additions & 35 deletions ts/features/barcode/components/BarcodeScanBaseScreenComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
IOColors,
IconButton,
LoadingSpinner,
TabItem,
TabNavigation
} from "@pagopa/io-app-design-system";
Expand Down Expand Up @@ -57,6 +58,7 @@ import {
trackBarcodeScanTorch,
trackZendeskSupport
} from "../analytics";
import { useCameraPermissionStatus } from "../hooks/useCameraPermissionStatus";
import { useIOBarcodeCameraScanner } from "../hooks/useIOBarcodeCameraScanner";
import {
IOBarcode,
Expand Down Expand Up @@ -209,21 +211,20 @@ const BarcodeScanBaseScreenComponent = ({
};

const {
cameraComponent,
cameraPermissionStatus,
requestCameraPermission,
openCameraSettings,
hasTorch,
isTorchOn,
toggleTorch
} = useIOBarcodeCameraScanner({
onBarcodeSuccess,
onBarcodeError,
barcodeFormats,
barcodeTypes,
isDisabled: isAppInBackground || !isFocused || isDisabled,
isLoading
});
openCameraSettings
} = useCameraPermissionStatus();

const { cameraComponent, hasTorch, isTorchOn, toggleTorch } =
useIOBarcodeCameraScanner({
onBarcodeSuccess,
onBarcodeError,
barcodeFormats,
barcodeTypes,
isDisabled: isAppInBackground || !isFocused || isDisabled,
isLoading
});

const customGoBack = (
<IconButton
Expand All @@ -234,11 +235,6 @@ const BarcodeScanBaseScreenComponent = ({
/>
);

const openAppSetting = useCallback(async () => {
// Open the custom settings if the app has one
await openCameraSettings();
}, [openCameraSettings]);

const cameraView = useMemo(() => {
if (cameraPermissionStatus === "granted") {
return cameraComponent;
Expand Down Expand Up @@ -266,26 +262,30 @@ const BarcodeScanBaseScreenComponent = ({
);
}

trackBarcodeCameraAuthorizationDenied();
if (cameraPermissionStatus === "denied") {
trackBarcodeCameraAuthorizationDenied();

return (
<CameraPermissionView
pictogram="cameraDenied"
title={I18n.t("barcodeScan.permissions.denied.title")}
body={I18n.t("barcodeScan.permissions.denied.label")}
action={{
label: I18n.t("barcodeScan.permissions.denied.action"),
accessibilityLabel: I18n.t("barcodeScan.permissions.denied.action"),
onPress: async () => {
trackBarcodeCameraAuthorizedFromSettings();
await openAppSetting();
}
}}
/>
);
return (
<CameraPermissionView
pictogram="cameraDenied"
title={I18n.t("barcodeScan.permissions.denied.title")}
body={I18n.t("barcodeScan.permissions.denied.label")}
action={{
label: I18n.t("barcodeScan.permissions.denied.action"),
accessibilityLabel: I18n.t("barcodeScan.permissions.denied.action"),
onPress: () => {
trackBarcodeCameraAuthorizedFromSettings();
openCameraSettings();
}
}}
/>
);
}

return <LoadingSpinner size={76} color="white" />;
}, [
cameraPermissionStatus,
openAppSetting,
openCameraSettings,
cameraComponent,
requestCameraPermission
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,12 @@ import { fireEvent } from "@testing-library/react-native";
import { View } from "react-native";
import configureMockStore from "redux-mock-store";
import I18n from "../../../../i18n";
import ROUTES from "../../../../navigation/routes";
import { applicationChangeState } from "../../../../store/actions/application";
import { appReducer } from "../../../../store/reducers";
import { GlobalState } from "../../../../store/reducers/types";

jest.mock("../../hooks/useIOBarcodeCameraScanner", () => ({
useIOBarcodeCameraScanner: jest.fn()
}));

import ROUTES from "../../../../navigation/routes";
import { renderScreenWithNavigationStoreContext } from "../../../../utils/testWrapper";
import { useCameraPermissionStatus } from "../../hooks/useCameraPermissionStatus";
import {
IOBarcodeCameraScanner,
useIOBarcodeCameraScanner
Expand All @@ -20,12 +16,23 @@ import { BarcodeScanBaseScreenComponent } from "../BarcodeScanBaseScreenComponen

const mockCameraComponent = <View testID="cameraComponentTestID" />;

jest.mock("../../hooks/useIOBarcodeCameraScanner", () => ({
useIOBarcodeCameraScanner: jest.fn()
}));

jest.mock("../../hooks/useCameraPermissionStatus", () => ({
useCameraPermissionStatus: jest.fn()
}));

(useCameraPermissionStatus as jest.Mock).mockImplementation(() => ({
cameraPermissionStatus: "granted",
requestCameraPermission: jest.fn(),
openCameraSettings: jest.fn()
}));

(useIOBarcodeCameraScanner as jest.Mock).mockImplementation(
(): IOBarcodeCameraScanner => ({
cameraComponent: mockCameraComponent,
cameraPermissionStatus: "granted",
requestCameraPermission: jest.fn(),
openCameraSettings: jest.fn(),
hasTorch: false,
isTorchOn: false,
toggleTorch: () => null
Expand Down Expand Up @@ -53,17 +60,11 @@ describe("Test BarcodeScanBaseScreenComponent", () => {
const mockRequestCameraPermission = jest.fn();
const mockOpenCameraSettings = jest.fn();

(useIOBarcodeCameraScanner as jest.Mock).mockImplementationOnce(
(): IOBarcodeCameraScanner => ({
cameraComponent: <View testID="cameraComponentTestID" />,
cameraPermissionStatus: "not-determined",
requestCameraPermission: mockRequestCameraPermission,
openCameraSettings: mockOpenCameraSettings,
hasTorch: false,
isTorchOn: false,
toggleTorch: () => null
})
);
(useCameraPermissionStatus as jest.Mock).mockImplementation(() => ({
cameraPermissionStatus: "not-determined",
requestCameraPermission: mockRequestCameraPermission,
openCameraSettings: mockOpenCameraSettings
}));

const { component } = renderComponent();

Expand Down Expand Up @@ -93,17 +94,11 @@ describe("Test BarcodeScanBaseScreenComponent", () => {
const mockRequestCameraPermission = jest.fn();
const mockOpenCameraSettings = jest.fn();

(useIOBarcodeCameraScanner as jest.Mock).mockImplementationOnce(
(): IOBarcodeCameraScanner => ({
cameraComponent: <View testID="cameraComponentTestID" />,
cameraPermissionStatus: "restricted",
requestCameraPermission: mockRequestCameraPermission,
openCameraSettings: mockOpenCameraSettings,
hasTorch: false,
isTorchOn: false,
toggleTorch: () => null
})
);
(useCameraPermissionStatus as jest.Mock).mockImplementation(() => ({
cameraPermissionStatus: "denied",
requestCameraPermission: mockRequestCameraPermission,
openCameraSettings: mockOpenCameraSettings
}));

const { component } = renderComponent();

Expand Down
68 changes: 68 additions & 0 deletions ts/features/barcode/hooks/useCameraPermissionStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { useCallback, useEffect, useState } from "react";
import { AppState, Linking } from "react-native";
import { Camera, CameraPermissionStatus } from "react-native-vision-camera";
import { isAndroid } from "../../../utils/platform";

/**
* Hook to handle camera permission status with platform specific behavior
*/
export const useCameraPermissionStatus = () => {
const [cameraPermissionStatus, setCameraPermissionStatus] =
useState<CameraPermissionStatus>();

/**
* Opens the system prompt to ask camera permission
*/
const requestCameraPermission = useCallback(async () => {
const permissions = await Camera.requestCameraPermission();
setCameraPermissionStatus(permissions);
}, []);

/**
* Opens the system settings to allow user to change the camera permission
*/
const openCameraSettings = useCallback(() => {
void Linking.openSettings();
}, []);

/**
* Checks the camera permission on mount
*
* **Note:** On android devices the app starts with a "denied" permission status,
* which does not necessarily mean that the permission was denied by the user.
* We need to request the permission to check if the user has denied it.
*/
useEffect(() => {
const permission = Camera.getCameraPermissionStatus();
if (isAndroid && permission === "denied") {
// Waits 500ms to ensure navigation is completed
setTimeout(requestCameraPermission, 500);
} else {
setCameraPermissionStatus(permission);
}
}, [requestCameraPermission]);

/**
* Setup listener for app state changes to detect if camera permissions were granted
* through system settings after the user returns to the app.
*/
useEffect(() => {
if (cameraPermissionStatus === "denied") {
const unsubscribe = AppState.addEventListener("change", nextAppState => {
if (nextAppState === "active") {
const permission = Camera.getCameraPermissionStatus();
setCameraPermissionStatus(permission);
}
});

return () => unsubscribe.remove();
}
return () => null;
}, [cameraPermissionStatus]);

return {
cameraPermissionStatus,
requestCameraPermission,
openCameraSettings
};
};
46 changes: 1 addition & 45 deletions ts/features/barcode/hooks/useIOBarcodeCameraScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ import {
useRef,
useState
} from "react";
import { Linking, StyleSheet, View } from "react-native";
import { StyleSheet, View } from "react-native";
import Animated, { FadeIn } from "react-native-reanimated";
import {
Camera,
CameraPermissionStatus,
Code,
CodeType,
useCameraDevice,
Expand Down Expand Up @@ -92,18 +91,6 @@ export type IOBarcodeCameraScanner = {
* Component that renders the camera
*/
cameraComponent: ReactNode;
/**
* Camera permission status
*/
cameraPermissionStatus: CameraPermissionStatus;
/**
* Opens the system prompt that let user to allow/deny camera permission
*/
requestCameraPermission: () => Promise<void>;
/**
* Opens the system settings screen to let user to change camera permission
*/
openCameraSettings: () => Promise<void>;
/**
* Returns true if the device has a torch
*/
Expand Down Expand Up @@ -199,9 +186,6 @@ export const useIOBarcodeCameraScanner = ({
const scannerReactivateTimeoutHandler = useRef<number>();
const [isResting, setIsResting] = useState(false);

const [cameraPermissionStatus, setCameraPermissionStatus] =
useState<CameraPermissionStatus>("not-determined");

/**
* Handles the detected {@link Code} and converts it to {@link IOBarcode}
* Returns an Either with the {@link BarcodeFailure} or the {@link IOBarcode}
Expand Down Expand Up @@ -283,14 +267,6 @@ export const useIOBarcodeCameraScanner = ({
onCodeScanned: handleScannedBarcodes
});

/**
* Hook that checks the camera permission on mount
*/
useEffect(() => {
const permission = Camera.getCameraPermissionStatus();
setCameraPermissionStatus(permission);
}, []);

/**
* Hook that clears the timeout handler on unmount
*/
Expand All @@ -301,23 +277,6 @@ export const useIOBarcodeCameraScanner = ({
[scannerReactivateTimeoutHandler]
);

/**
* Opens the system prompt to ask camera permission
*/
const requestCameraPermission = async () => {
const permissions = await Camera.requestCameraPermission();
setCameraPermissionStatus(permissions);
};

/**
* Opens the settings page to allow user to change the camer settings
*/
const openCameraSettings = async () => {
await Linking.openSettings();
const permissions = Camera.getCameraPermissionStatus();
setCameraPermissionStatus(permissions);
};

/**
* Component that renders camera and marker
*/
Expand Down Expand Up @@ -349,9 +308,6 @@ export const useIOBarcodeCameraScanner = ({

return {
cameraComponent,
cameraPermissionStatus,
requestCameraPermission,
openCameraSettings,
hasTorch,
isTorchOn,
toggleTorch
Expand Down
23 changes: 7 additions & 16 deletions ts/features/barcode/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import { useIOBarcodeCameraScanner } from "./hooks/useIOBarcodeCameraScanner";
import { useIOBarcodeFileReader } from "./hooks/useIOBarcodeFileReader";
import { IOBarcodeFormat, IOBarcodeType, IOBarcode } from "./types/IOBarcode";
import { BarcodeScanBaseScreenComponent } from "./components/BarcodeScanBaseScreenComponent";
import { useIOBarcodeFileReader } from "./hooks/useIOBarcodeFileReader";
import { IOBarcode, IOBarcodeFormat, IOBarcodeType } from "./types/IOBarcode";
import { BarcodeFailure } from "./types/failure";
import {
getIOBarcodesByType,
IOBarcodesByType
} from "./utils/getBarcodesByType";
import { IOBarcodesByType } from "./utils/getBarcodesByType";

export { BarcodeScanBaseScreenComponent, useIOBarcodeFileReader };
export type {
IOBarcodeType,
IOBarcodeFormat,
IOBarcode,
BarcodeFailure,
IOBarcode,
IOBarcodeFormat,
IOBarcodeType,
IOBarcodesByType
};
export {
useIOBarcodeCameraScanner,
useIOBarcodeFileReader,
BarcodeScanBaseScreenComponent,
getIOBarcodesByType
};
Loading