Skip to content

Commit

Permalink
fixup! feat(suite-native): qr code scanner bottom sheet
Browse files Browse the repository at this point in the history
  • Loading branch information
PeKne committed Sep 9, 2024
1 parent ce9aa0f commit 22fe1f4
Show file tree
Hide file tree
Showing 9 changed files with 78 additions and 82 deletions.
4 changes: 4 additions & 0 deletions suite-native/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,17 @@ const getPlugins = (): ExpoPlugins => {
'expo-camera',
{
cameraPermission: 'Allow $(PRODUCT_NAME) to access camera for QR code scanning.',
microphonePermission: false,
recordAudioAndroid: false,
},
],
[
'expo-image-picker',
{
photosPermission:
'Allow $(PRODUCT_NAME) to access your photos to let you import QR code images.',
microphonePermission: false,
cameraPermission: false,
},
],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import { useState } from 'react';
import { Icon } from '@suite-common/icons-deprecated';
import { ScanQRBottomSheet } from '@suite-native/qr-code';
import { Translation } from '@suite-native/intl';
import { useNativeStyles } from '@trezor/styles';

type QrCodeBottomSheetIconProps = {
onCodeScanned: (data: string) => void;
};

export const QrCodeBottomSheetIcon = ({ onCodeScanned }: QrCodeBottomSheetIconProps) => {
const { utils } = useNativeStyles();
const [isVisible, setIsVisible] = useState(false);

const toggleBottomSheet = () => {
Expand All @@ -18,7 +20,7 @@ export const QrCodeBottomSheetIcon = ({ onCodeScanned }: QrCodeBottomSheetIconPr

return (
<>
<TouchableOpacity onPress={toggleBottomSheet}>
<TouchableOpacity onPress={toggleBottomSheet} hitSlop={utils.spacings.small}>
<Icon name="qrCode" size="large" />
</TouchableOpacity>

Expand Down
3 changes: 0 additions & 3 deletions suite-native/navigation/src/navigators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,6 @@ export type AccountsImportStackParamList = {
qrCode?: string;
networkSymbol: NetworkSymbol;
};
[AccountsImportStackRoutes.XpubScanModal]: {
networkSymbol: NetworkSymbol;
};
[AccountsImportStackRoutes.AccountImportLoading]: {
xpubAddress: XpubAddress;
networkSymbol: NetworkSymbol;
Expand Down
1 change: 0 additions & 1 deletion suite-native/navigation/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ export enum OnboardingStackRoutes {
export enum AccountsImportStackRoutes {
SelectNetwork = 'SelectNetwork',
XpubScan = 'XpubScan',
XpubScanModal = 'XpubScanModal',
AccountImportLoading = 'AccountImportLoading',
AccountImportSummary = 'AccountImportSummary',
}
Expand Down
17 changes: 11 additions & 6 deletions suite-native/qr-code/src/components/CameraPermissionError.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { Linking } from 'react-native';

import { Box, Button, Text } from '@suite-native/atoms';
import { prepareNativeStyle, useNativeStyles } from '@trezor/styles';

type CameraPermissionErrorProps = {
onPermissionRequest: () => void;
};

const permissionTextContainerStyle = prepareNativeStyle(({ spacings }) => ({
paddingTop: spacings.extraLarge,
}));
Expand All @@ -13,15 +11,22 @@ const grantPermissionButtonStyle = prepareNativeStyle(({ spacings }) => ({
marginTop: spacings.large,
}));

export const CameraPermissionError = ({ onPermissionRequest }: CameraPermissionErrorProps) => {
export const CameraPermissionError = () => {
const { applyStyle } = useNativeStyles();

const navigateToSystemSettings = () => {
Linking.openSettings();
};

return (
<Box style={applyStyle(permissionTextContainerStyle)}>
<Text textAlign="center">Camera access denied.</Text>
<Text textAlign="center">Please allow camera access in your device settings.</Text>

<Button onPress={onPermissionRequest} style={applyStyle(grantPermissionButtonStyle)}>
<Button
onPress={navigateToSystemSettings}
style={applyStyle(grantPermissionButtonStyle)}
>
Grant permission
</Button>
</Box>
Expand Down
15 changes: 10 additions & 5 deletions suite-native/qr-code/src/components/PickQRFromGalleryButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BarcodeScanningResult, scanFromURLAsync } from 'expo-camera';
import { scanFromURLAsync } from 'expo-camera';
import * as ImagePicker from 'expo-image-picker';

import { Button } from '@suite-native/atoms';
Expand All @@ -7,10 +7,14 @@ import { useToast } from '@suite-native/toasts';
import { Translation } from '@suite-native/intl';

type PickQRFromGalleryButtonProps = {
onImagePicked: (data: BarcodeScanningResult) => void;
onImagePicked: (data: string) => void;
onError: () => void;
};

export const PickQRFromGalleryButton = ({ onImagePicked }: PickQRFromGalleryButtonProps) => {
export const PickQRFromGalleryButton = ({
onImagePicked,
onError,
}: PickQRFromGalleryButtonProps) => {
const { showToast } = useToast();

const handlePickImage = async () => {
Expand All @@ -19,10 +23,11 @@ export const PickQRFromGalleryButton = ({ onImagePicked }: PickQRFromGalleryButt

try {
const scannedResults = await scanFromURLAsync(imageUri!, ['qr']);
const QrData = scannedResults[0];
const { data } = scannedResults[0];

onImagePicked(QrData);
onImagePicked(data);
} catch (error) {
onError();
showToast({
variant: 'error',
icon: 'warningTriangle',
Expand Down
54 changes: 24 additions & 30 deletions suite-native/qr-code/src/components/QRCodeScanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ import { Translation } from '@suite-native/intl';

import { CameraPermissionError } from './CameraPermissionError';
import { useCameraPermission } from '../hooks/useCameraPermission';
import { PickQRFromGalleryButton } from './PickQRFromGalleryButton';

type QRCodeScannerProps = {
onCodeScanned: (data: string) => void;
};

const SPACING = 50;

const SCANNER_SIZE = Dimensions.get('screen').width - nativeSpacings.medium * 2;

const cameraContainerStyle = prepareNativeStyle(utils => ({
Expand All @@ -33,7 +30,8 @@ const cameraStyle = prepareNativeStyle(() => ({

export const QRCodeScanner = ({ onCodeScanned }: QRCodeScannerProps) => {
const { applyStyle } = useNativeStyles();
const { cameraPermissionStatus, requestCameraPermission } = useCameraPermission();
const { cameraPermissionStatus } = useCameraPermission();

const [scanned, setScanned] = useState(false);
// We don't need wait on iOS, check comment in useEffect lower for more details
const [isCameraLoading, setIsCameraLoading] = useState(Platform.OS === 'android');
Expand Down Expand Up @@ -69,36 +67,32 @@ export const QRCodeScanner = ({ onCodeScanned }: QRCodeScannerProps) => {

case PermissionStatus.GRANTED:
return (
<VStack spacing={SPACING} paddingTop="extraLarge">
<VStack spacing="medium" justifyContent="center">
<HStack alignItems="center" justifyContent="center">
<Icon
name="lightbulb"
color="backgroundSecondaryDefault"
size="mediumLarge"
/>
<Text color="backgroundSecondaryDefault">
<Translation id="qrCode.qrCodeHint" />
</Text>
</HStack>
<Box style={applyStyle(cameraContainerStyle)}>
<CameraView
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
style={applyStyle(cameraStyle)}
barcodeScannerSettings={{
barcodeTypes: ['qr'],
}}
accessibilityLabel="QR code scanner"
/>
</Box>
</VStack>

<PickQRFromGalleryButton onImagePicked={handleBarCodeScanned} />
<VStack spacing="medium" justifyContent="center">
<HStack alignItems="center" justifyContent="center">
<Icon
name="lightbulb"
color="backgroundSecondaryDefault"
size="mediumLarge"
/>
<Text color="backgroundSecondaryDefault">
<Translation id="qrCode.qrCodeHint" />
</Text>
</HStack>
<Box style={applyStyle(cameraContainerStyle)}>
<CameraView
onBarcodeScanned={scanned ? undefined : handleBarCodeScanned}
style={applyStyle(cameraStyle)}
barcodeScannerSettings={{
barcodeTypes: ['qr'],
}}
accessibilityLabel="QR code scanner"
/>
</Box>
</VStack>
);

case PermissionStatus.DENIED:
default:
return <CameraPermissionError onPermissionRequest={requestCameraPermission} />;
return <CameraPermissionError />;
}
};
25 changes: 11 additions & 14 deletions suite-native/qr-code/src/components/ScanQRBottomSheet.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React, { ReactNode, useEffect } from 'react';
import React, { ReactNode } from 'react';

import { PermissionStatus } from 'expo-camera';

import { BottomSheet, Text } from '@suite-native/atoms';
import { BottomSheet, Text, VStack } from '@suite-native/atoms';

import { QRCodeScanner } from './QRCodeScanner';
import { useCameraPermission } from '../hooks/useCameraPermission';
import { PickQRFromGalleryButton } from './PickQRFromGalleryButton';

type ScanQRBottomSheetProps = {
title: ReactNode;
Expand All @@ -14,6 +12,8 @@ type ScanQRBottomSheetProps = {
onCodeScanned: (data: string) => void;
};

const SPACING = 50;

export const ScanQRBottomSheet = ({
title,
isVisible,
Expand All @@ -25,21 +25,18 @@ export const ScanQRBottomSheet = ({
onClose();
};

const { requestCameraPermission, cameraPermissionStatus } = useCameraPermission();

useEffect(() => {
if (isVisible && cameraPermissionStatus !== PermissionStatus.GRANTED) {
requestCameraPermission();
}
}, [isVisible, cameraPermissionStatus, requestCameraPermission]);

return (
<BottomSheet
isVisible={isVisible}
onClose={onClose}
title={<Text variant="highlight">{title}</Text>}
>
<QRCodeScanner onCodeScanned={handleCodeScanned} />
{isVisible && ( // conditionally rendered so the inside hooks are not triggered until is the bottom sheet displayed.
<VStack spacing={SPACING} paddingTop="extraLarge">
<QRCodeScanner onCodeScanned={handleCodeScanned} />
<PickQRFromGalleryButton onImagePicked={handleCodeScanned} onError={onClose} />
</VStack>
)}
</BottomSheet>
);
};
37 changes: 15 additions & 22 deletions suite-native/qr-code/src/hooks/useCameraPermission.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,39 @@
import { useCallback, useEffect, useState } from 'react';
import { AppState, Linking } from 'react-native';
import { useEffect, useState } from 'react';
import { AppState } from 'react-native';

import { Camera, PermissionStatus } from 'expo-camera';

export const useCameraPermission = () => {
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<PermissionStatus>(
const [cameraPermissionStatus, setCameraPermissionStatus] = useState(
PermissionStatus.UNDETERMINED,
);

const checkCameraPermissionStatus = useCallback(async () => {
const { status } = await Camera.requestCameraPermissionsAsync();

setCameraPermissionStatus(status);
useEffect(() => {
const invokeCameraPermissionDialog = async () => {
const { status } = await Camera.requestCameraPermissionsAsync();
setCameraPermissionStatus(status);
};

return status;
invokeCameraPermissionDialog();
}, []);

useEffect(() => {
// When we go back from settings we need to check if the permission was granted.
const subscription = AppState.addEventListener('change', nextAppState => {
const subscription = AppState.addEventListener('change', async nextAppState => {
if (nextAppState === 'active') {
checkCameraPermissionStatus();
// `getCameraPermissionsAsync` has to be called instead of `requestCameraPermissionsAsync`!
// `requestCameraPermissionsAsync` is triggering system dialog which changes the AppState to background and causes infinite loop of this event listener.
const { status } = await Camera.getCameraPermissionsAsync();
setCameraPermissionStatus(status);
}
});

return () => {
subscription.remove();
};
}, [checkCameraPermissionStatus]);

const requestCameraPermission = useCallback(async () => {
const status = await checkCameraPermissionStatus();
if (status === PermissionStatus.UNDETERMINED) {
await Camera.requestCameraPermissionsAsync();
} else if (status === PermissionStatus.DENIED) {
await Linking.openSettings();
}
checkCameraPermissionStatus();
}, [checkCameraPermissionStatus]);
}, []);

return {
cameraPermissionStatus,
requestCameraPermission,
};
};

0 comments on commit 22fe1f4

Please sign in to comment.