Merge pull request #51 from idpass/29-fix-location

App should ask for permission to location access when opening Scan QR page - added allow access button
This commit is contained in:
Paolo Miguel de Leon
2022-03-08 11:48:48 +08:00
committed by GitHub
5 changed files with 139 additions and 65 deletions

View File

@@ -1,10 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Camera } from 'expo-camera';
import { BarCodeEvent, BarCodeScanner } from 'expo-barcode-scanner';
import { Linking, StyleSheet, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { Colors } from './ui/styleUtils';
import { Button, Text } from './ui';
import { Column, Button, Text } from './ui';
import { GlobalContext } from '../shared/GlobalContext';
import { useSelector } from '@xstate/react';
import { selectIsActive } from '../machines/app';
@@ -22,17 +22,12 @@ const styles = StyleSheet.create({
scanner: {
height: 400,
width: '100%',
margin: 'auto'
margin: 'auto',
},
buttonContainer: {
height: '100%',
width: '100%',
},
flipButtonContainer: {
position: 'absolute',
width: '80%',
top: '110%',
},
buttonStyle: {
position: 'absolute',
width: '100%',
@@ -95,13 +90,13 @@ export const QrScanner: React.FC<QrScannerProps> = (props) => {
<Camera
style={styles.scanner}
barCodeScannerSettings={{
barcodeTypes: [BarCodeScanner.Constants.BarCodeType.qr]
barcodeTypes: [BarCodeScanner.Constants.BarCodeType.qr],
}}
onBarCodeScanned={scanned ? undefined : onBarcodeScanned}
type={type}
/>
/>
</View>
<View style={styles.flipButtonContainer}>
<Column margin="24 0">
<TouchableOpacity
style={styles.button}
onPress={() => {
@@ -113,7 +108,7 @@ export const QrScanner: React.FC<QrScannerProps> = (props) => {
}}>
<Icon name="flip-camera-ios" color={Colors.Black} size={64} />
</TouchableOpacity>
</View>
</Column>
</View>
);

View File

@@ -5,7 +5,7 @@ import {
getDeviceName,
getDeviceNameSync,
} from 'react-native-device-info';
import { EventFrom, spawn, StateFrom } from 'xstate';
import { EventFrom, spawn, StateFrom, send } from 'xstate';
import { createModel } from 'xstate/lib/model';
import { authMachine, createAuthMachine } from './auth';
import { createSettingsMachine, settingsMachine } from './settings';
@@ -14,7 +14,7 @@ import { createVidMachine, vidMachine } from './vid';
import { createActivityLogMachine, activityLogMachine } from './activityLog';
import { createRequestMachine, requestMachine } from './request';
import { createScanMachine, scanMachine } from './scan';
import { respond } from 'xstate/lib/actions';
import { pure, respond } from 'xstate/lib/actions';
import { AppServices } from '../shared/GlobalContext';
const model = createModel(
@@ -98,8 +98,12 @@ export const appMachine = model.createMachine(
initial: 'checking',
states: {
checking: {},
active: {},
inactive: {},
active: {
entry: ['forwardToServices'],
},
inactive: {
entry: ['forwardToServices'],
},
},
},
network: {
@@ -113,8 +117,12 @@ export const appMachine = model.createMachine(
initial: 'checking',
states: {
checking: {},
online: {},
offline: {},
online: {
entry: ['forwardToServices'],
},
offline: {
entry: ['forwardToServices'],
},
},
},
},
@@ -123,6 +131,12 @@ export const appMachine = model.createMachine(
},
{
actions: {
forwardToServices: pure((context, event) =>
Object.values(context.serviceRefs).map((serviceRef) =>
send({ ...event, type: `APP_${event.type}` }, { to: serviceRef })
)
),
requestDeviceInfo: respond((context) => ({
type: 'RECEIVE_DEVICE_INFO',
info: {

View File

@@ -2,7 +2,7 @@ import SmartShare from '@idpass/smartshare-react-native';
import LocationEnabler from 'react-native-location-enabler';
import { EventFrom, send, sendParent, StateFrom } from 'xstate';
import { createModel } from 'xstate/lib/model';
import { EmitterSubscription } from 'react-native';
import { EmitterSubscription, Linking, PermissionsAndroid } from 'react-native';
import { DeviceInfo } from '../components/DeviceInfoList';
import { Message } from '../shared/Message';
import { getDeviceNameSync } from 'react-native-device-info';
@@ -44,8 +44,10 @@ const model = createModel(
UPDATE_REASON: (reason: string) => ({ reason }),
LOCATION_ENABLED: () => ({}),
LOCATION_DISABLED: () => ({}),
LOCATION_REQUEST: () => ({}),
UPDATE_VID_NAME: (vidName: string) => ({ vidName }),
STORE_RESPONSE: (response: any) => ({ response }),
APP_ACTIVE: () => ({}),
},
}
);
@@ -73,26 +75,44 @@ export const scanMachine = model.createMachine(
},
checkingLocationService: {
invoke: {
src: 'checkLocationService',
src: 'checkLocationStatus',
},
on: {
LOCATION_ENABLED: '.enabled',
},
initial: 'checking',
initial: 'checkingStatus',
states: {
checking: {
checkingStatus: {
on: {
LOCATION_DISABLED: 'requesting',
LOCATION_ENABLED: 'checkingPermission',
LOCATION_DISABLED: 'requestingToEnable',
},
},
requesting: {
entry: ['requestLocationService'],
requestingToEnable: {
entry: ['requestToEnableLocation'],
on: {
LOCATION_DISABLED: '#locationDenied',
LOCATION_ENABLED: 'checkingPermission',
LOCATION_DISABLED: 'disabled',
},
},
enabled: {
always: '#clearingConnection',
checkingPermission: {
invoke: {
src: 'checkLocationPermission',
},
on: {
LOCATION_ENABLED: '#clearingConnection',
LOCATION_DISABLED: 'denied',
},
},
denied: {
on: {
LOCATION_REQUEST: {
actions: ['openSettings'],
},
APP_ACTIVE: 'checkingPermission',
},
},
disabled: {
on: {
LOCATION_REQUEST: 'requestingToEnable',
},
},
},
},
@@ -117,9 +137,6 @@ export const scanMachine = model.createMachine(
],
},
},
locationDenied: {
id: 'locationDenied',
},
preparingToConnect: {
entry: ['requestSenderInfo'],
on: {
@@ -225,7 +242,7 @@ export const scanMachine = model.createMachine(
senderInfo: (_, event: ReceiveDeviceInfoEvent) => event.info,
}),
requestLocationService: (context) => {
requestToEnableLocation: (context) => {
LocationEnabler.requestResolutionSettings(context.locationConfig);
},
@@ -301,10 +318,40 @@ export const scanMachine = model.createMachine(
}),
{ to: (context) => context.serviceRefs.activityLog }
),
openSettings: () => {
Linking.openSettings();
},
},
services: {
checkLocationService: (context) => (callback) => {
checkLocationPermission: () => async (callback) => {
try {
// TODO: a more reliable way to wait for animation to finish when app becomes active
await new Promise((resolve) => setTimeout(resolve, 250));
const response = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location access',
message:
'Location access is required for the scanning functionality.',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
if (response === 'granted') {
callback(model.events.LOCATION_ENABLED());
} else {
callback(model.events.LOCATION_DISABLED());
}
} catch (e) {
console.error(e);
}
},
checkLocationStatus: (context) => (callback) => {
const listener = LocationEnabler.addListener(({ locationEnabled }) => {
if (locationEnabled) {
callback(model.events.LOCATION_ENABLED());
@@ -459,6 +506,10 @@ export function selectInvalid(state: State) {
return state.matches('invalid');
}
export function selectLocationDenied(state: State) {
return state.matches('locationDenied');
export function selectIsLocationDenied(state: State) {
return state.matches('checkingLocationService.denied');
}
export function selectIsLocationDisabled(state: State) {
return state.matches('checkingLocationService.disabled');
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { QrScanner } from '../../components/QrScanner';
import { Column, Text } from '../../components/ui';
import { Button, Column, Text } from '../../components/ui';
import { Colors } from '../../components/ui/styleUtils';
import { MainRouteProps } from '../../routes/main';
import { MessageOverlay } from '../../components/MessageOverlay';
@@ -11,21 +11,27 @@ export const ScanScreen: React.FC<MainRouteProps> = (props) => {
const controller = useScanScreen(props);
return (
<Column fill padding="98 24" backgroundColor={Colors.LightGrey}>
<Column>
<Text align="center">Scan QR Code</Text>
{controller.isLocationDenied && (
<Column fill padding="98 24 24 24" backgroundColor={Colors.LightGrey}>
<Text align="center">Scan QR Code</Text>
{controller.isLocationDisabled || controller.isLocationDenied ? (
<Column fill align="space-between">
<Text align="center" margin="16 0" color={Colors.Red}>
Location access is required for the scanning functionality.
{controller.locationError.message}
</Text>
)}
</Column>
{!controller.isEmpty ? (
<Column fill padding="16 0" crossAlign="center">
{controller.isScanning ? (
<QrScanner onQrFound={controller.SCAN} />
) : null}
<Button
title={controller.locationError.button}
onPress={controller.LOCATION_REQUEST}
/>
</Column>
) : null}
{!controller.isEmpty ? (
controller.isScanning && (
<Column fill padding="16 0" crossAlign="center">
<QrScanner onQrFound={controller.SCAN} />
</Column>
)
) : (
<Text align="center" margin="16 0" color={Colors.Red}>
No sharable {controller.vidLabel.plural} are available.
@@ -36,7 +42,7 @@ export const ScanScreen: React.FC<MainRouteProps> = (props) => {
isVisible={controller.statusMessage !== ''}
message={controller.statusMessage}
hasProgress={!controller.isInvalid}
onBackdropPress={controller.onDismissInvalid}
onBackdropPress={controller.isInvalid && controller.DISMISS}
/>
<SendVidModal

View File

@@ -3,7 +3,8 @@ import { useContext, useEffect } from 'react';
import {
ScanEvents,
selectInvalid,
selectLocationDenied,
selectIsLocationDisabled,
selectIsLocationDenied,
selectReviewing,
selectScanning,
selectStatusMessage,
@@ -22,6 +23,20 @@ export function useScanScreen({ navigation }: MainRouteProps) {
const shareableVids = useSelector(vidService, selectShareableVids);
const isInvalid = useSelector(scanService, selectInvalid);
const isLocationDisabled = useSelector(scanService, selectIsLocationDisabled);
const isLocationDenied = useSelector(scanService, selectIsLocationDenied);
const locationError = { message: '', button: '' };
if (isLocationDisabled) {
locationError.message =
'Location services must be enabled for the scanning functionality';
locationError.button = 'Enable location services';
} else if (isLocationDenied) {
locationError.message =
'Location permission is required for the scanning functionality';
locationError.button = 'Allow access to location';
}
useEffect(() => {
const subscriptions = [
navigation.addListener('focus', () =>
@@ -45,26 +60,19 @@ export function useScanScreen({ navigation }: MainRouteProps) {
}, []);
return {
locationError,
statusMessage: useSelector(scanService, selectStatusMessage),
vidLabel: useSelector(settingsService, selectVidLabel),
onDismissInvalid: () => {
if (isInvalid) {
DISMISS();
}
},
isInvalid,
isEmpty: !shareableVids.length,
isLocationDisabled,
isLocationDenied,
isScanning: useSelector(scanService, selectScanning),
isReviewing: useSelector(scanService, selectReviewing),
isLocationDenied: useSelector(scanService, selectLocationDenied),
DISMISS,
DISMISS: () => scanService.send(ScanEvents.DISMISS()),
LOCATION_REQUEST: () => scanService.send(ScanEvents.LOCATION_REQUEST()),
SCAN: (qrCode: string) => scanService.send(ScanEvents.SCAN(qrCode)),
};
function DISMISS() {
scanService.send(ScanEvents.DISMISS());
}
}