feat: verify identity on both sides

This commit is contained in:
Paolo Miguel de Leon
2022-10-31 15:18:15 +08:00
parent 0bead0610e
commit c83731a250
20 changed files with 361 additions and 111 deletions

View File

@@ -16,6 +16,7 @@ import {
createFaceScannerMachine,
selectIsInvalid,
selectIsCapturing,
selectIsVerifying,
} from '../machines/faceScanner';
import { GlobalContext } from '../shared/GlobalContext';
import { selectIsActive } from '../machines/app';
@@ -37,6 +38,7 @@ export const FaceScanner: React.FC<FaceScannerProps> = (props) => {
const isCheckingPermission = useSelector(service, selectIsCheckingPermission);
const isScanning = useSelector(service, selectIsScanning);
const isCapturing = useSelector(service, selectIsCapturing);
const isVerifying = useSelector(service, selectIsVerifying);
const setCameraRef = useCallback(
(node: Camera) => {
@@ -79,7 +81,6 @@ export const FaceScanner: React.FC<FaceScannerProps> = (props) => {
return (
<Column crossAlign="center">
<Text size="smaller">{t('takeSelfie')}</Text>
<Column style={[styles.scannerContainer]}>
<Camera
ratio="4:3"
@@ -89,7 +90,7 @@ export const FaceScanner: React.FC<FaceScannerProps> = (props) => {
/>
</Column>
<Centered margin="24 0">
{isCapturing ? (
{isCapturing || isVerifying ? (
<RotatingIcon name="sync" size={64} />
) : (
<Row crossAlign="center">

View File

@@ -23,6 +23,10 @@ const VerifiedIcon: React.FC = () => {
export const VcDetails: React.FC<VcDetailsProps> = (props) => {
const { t, i18n } = useTranslation('VcDetails');
if (props.vc?.verifiableCredential == null) {
return <Text align="center">Loading details...</Text>;
}
return (
<Column>
<Row pY={16} pX={8} align="space-between">

View File

@@ -1,3 +1,4 @@
export default function faceAuth(capturedImage: string, vcImage: string) {
export default async function faceAuth(capturedImage: string, vcImage: string) {
// TODO: iOS implementation
return Promise.resolve(true);
}

View File

@@ -5,6 +5,7 @@
"deviceRefNumber": "Device reference number",
"name": "Name"
},
"FaceScanner": {},
"OIDcAuth": {
"title": "OIDC Authentication",
"text": "To be replaced with the OIDC provider UI",
@@ -153,6 +154,7 @@
"ReceiveVcScreen": {
"header": "{{vcLabel}} details",
"acceptRequest": "Accept request and receive {{vcLabel}}",
"acceptRequestAndVerify": "Accept request and verify",
"reject": "Reject"
},
"RequestScreen": {
@@ -231,7 +233,11 @@
"rejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
},
}
}
},
"VerifyIdentityOverlay": {
"status": {
"verifyingIdentity": "Verifying identity..."
},
"errors": {
@@ -250,6 +256,12 @@
"cancel": "Cancel",
"save": "Save",
"editLabel": "Edit {{label}}",
"tryAgain": "Try again"
"tryAgain": "Try again",
"camera": {
"errors": {
"missingPermission": "This app uses the camera to scan the QR code of another device."
},
"allowAccess": "Allow access to camera"
}
}
}

View File

@@ -214,6 +214,7 @@ export const createFaceScannerMachine = (vcImage: string) =>
},
verifyImage: (context) => {
context.cameraRef.pausePreview();
const rxDataURI =
/data:(?<mime>[\w/\-.]+);(?<encoding>\w+),(?<data>.*)/;
const matches = rxDataURI.exec(vcImage).groups;
@@ -257,6 +258,10 @@ export function selectIsCapturing(state: State) {
return state.matches('capturing');
}
export function selectIsVerifying(state: State) {
return state.matches('verifying');
}
export function selectIsValid(state: State) {
return state.matches('valid');
}

View File

@@ -28,6 +28,8 @@ import {
offlineSend,
SendVcResponseEvent,
} from '../shared/smartshare';
import { log } from 'xstate/lib/actions';
// import { verifyPresentation } from '../shared/vcjs/verifyPresentation';
type SharingProtocol = 'OFFLINE' | 'ONLINE';
@@ -49,6 +51,7 @@ const model = createModel(
{
events: {
ACCEPT: () => ({}),
ACCEPT_AND_VERIFY: () => ({}),
REJECT: () => ({}),
CANCEL: () => ({}),
DISMISS: () => ({}),
@@ -67,6 +70,9 @@ const model = createModel(
VC_RESPONSE: (response: unknown) => ({ response }),
SWITCH_PROTOCOL: (value: boolean) => ({ value }),
GOTO_SETTINGS: () => ({}),
FACE_VALID: () => ({}),
FACE_INVALID: () => ({}),
RETRY_VERIFICATION: () => ({}),
},
}
);
@@ -79,6 +85,11 @@ export const requestMachine = model.createMachine(
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
services: {} as {
verifyVp: {
data: VC;
};
},
},
id: 'request',
initial: 'inactive',
@@ -195,7 +206,7 @@ export const requestMachine = model.createMachine(
DISCONNECT: 'disconnected',
VC_RECEIVED: {
target: 'reviewing',
actions: ['setIncomingVc'],
actions: 'setIncomingVc',
},
},
initial: 'inProgress',
@@ -219,12 +230,42 @@ export const requestMachine = model.createMachine(
reviewing: {
on: {
ACCEPT: '.accepting',
ACCEPT_AND_VERIFY: '.verifyingIdentity',
REJECT: '.rejected',
CANCEL: '.rejected',
},
initial: 'idle',
states: {
idle: {},
verifyingIdentity: {
on: {
FACE_VALID: {
target: 'verifyingVp',
},
FACE_INVALID: {
target: 'invalidIdentity',
},
CANCEL: 'idle',
},
},
invalidIdentity: {
on: {
DISMISS: 'idle',
RETRY_VERIFICATION: 'verifyingIdentity',
},
},
verifyingVp: {
invoke: {
src: 'verifyVp',
onDone: {
target: 'accepting',
},
onError: {
target: 'idle',
actions: log('Failed to verify Verifiable Presentation'),
},
},
},
accepting: {
initial: 'requestingReceivedVcs',
states: {
@@ -358,8 +399,16 @@ export const requestMachine = model.createMachine(
senderInfo: (_context, event) => event.senderInfo,
}),
setIncomingVc: model.assign({
incomingVc: (_context, event) => event.vc,
setIncomingVc: assign({
incomingVc: (_context, event) => {
const vp = event.vc.verifiablePresentation;
return vp != null
? {
...event.vc,
verifiableCredential: vp.verifiableCredential[0],
}
: event.vc;
},
}),
registerLoggers: assign({
@@ -594,6 +643,22 @@ export const requestMachine = model.createMachine(
onlineSend(event);
}
},
verifyVp: (context) => async () => {
const vp = context.incomingVc.verifiablePresentation;
// TODO
// const challenge = ?
// await verifyPresentation(vp, challenge);
const vc: VC = {
...context.incomingVc,
verifiablePresentation: null,
verifiableCredential: vp.verifiableCredential[0],
};
return Promise.resolve(vc);
},
},
guards: {
@@ -638,6 +703,10 @@ export function selectSharingProtocol(state: State) {
return state.context.sharingProtocol;
}
export function selectIsIncomingVp(state: State) {
return state.context.incomingVc?.verifiablePresentation != null;
}
export function selectIsReviewing(state: State) {
return state.matches('reviewing');
}
@@ -650,6 +719,18 @@ export function selectIsRejected(state: State) {
return state.matches('reviewing.rejected');
}
export function selectIsVerifyingIdentity(state: State) {
return state.matches('reviewing.verifyingIdentity');
}
export function selectIsVerifyingVp(state: State) {
return state.matches('reviewing.verifyingVp');
}
export function selectIsInvalidIdentity(state: State) {
return state.matches('reviewing.invalidIdentity');
}
export function selectIsDisconnected(state: State) {
return state.matches('disconnected');
}

View File

@@ -4,6 +4,15 @@ export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
'': { type: '' };
'done.invoke.request.reviewing.verifyingVp:invocation[0]': {
type: 'done.invoke.request.reviewing.verifyingVp:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.request.reviewing.verifyingVp:invocation[0]': {
type: 'error.platform.request.reviewing.verifyingVp:invocation[0]';
data: unknown;
};
'xstate.after(CLEAR_DELAY)#clearingConnection': {
type: 'xstate.after(CLEAR_DELAY)#clearingConnection';
};
@@ -20,6 +29,7 @@ export interface Typegen0 {
sendVcResponse:
| 'done.invoke.accepted:invocation[0]'
| 'done.invoke.request.reviewing.rejected:invocation[0]';
verifyVp: 'done.invoke.request.reviewing.verifyingVp:invocation[0]';
};
'missingImplementations': {
actions: never;
@@ -53,7 +63,9 @@ export interface Typegen0 {
| 'xstate.after(CLEAR_DELAY)#clearingConnection'
| 'xstate.init';
requestExistingVc: 'VC_RESPONSE';
requestReceivedVcs: 'ACCEPT';
requestReceivedVcs:
| 'ACCEPT'
| 'done.invoke.request.reviewing.verifyingVp:invocation[0]';
requestReceiverInfo: 'CONNECTED';
sendVcReceived: 'STORE_RESPONSE';
setIncomingVc: 'VC_RECEIVED';
@@ -74,6 +86,7 @@ export interface Typegen0 {
receiveVc: 'EXCHANGE_DONE';
requestBluetooth: 'BLUETOOTH_DISABLED';
sendVcResponse: 'CANCEL' | 'REJECT' | 'STORE_RESPONSE';
verifyVp: 'FACE_VALID';
};
'eventsCausingGuards': {
hasExistingVc: 'VC_RESPONSE';
@@ -104,8 +117,11 @@ export interface Typegen0 {
| 'reviewing.accepting.requestingReceivedVcs'
| 'reviewing.accepting.storingVc'
| 'reviewing.idle'
| 'reviewing.invalidIdentity'
| 'reviewing.navigatingToHome'
| 'reviewing.rejected'
| 'reviewing.verifyingIdentity'
| 'reviewing.verifyingVp'
| 'waitingForConnection'
| 'waitingForVc'
| 'waitingForVc.inProgress'
@@ -117,8 +133,11 @@ export interface Typegen0 {
| 'accepted'
| 'accepting'
| 'idle'
| 'invalidIdentity'
| 'navigatingToHome'
| 'rejected'
| 'verifyingIdentity'
| 'verifyingVp'
| {
accepting?:
| 'mergingIncomingVc'

View File

@@ -28,6 +28,7 @@ import {
import { check, PERMISSIONS, PermissionStatus } from 'react-native-permissions';
import { checkLocation, requestLocation } from '../shared/location';
import { CameraCapturedPicture } from 'expo-camera';
import { log } from 'xstate/lib/actions';
const findingConnectionId = '#scan.findingConnection';
const checkingLocationServiceId = '#checkingLocationService';
@@ -42,6 +43,7 @@ const model = createModel(
senderInfo: {} as DeviceInfo,
receiverInfo: {} as DeviceInfo,
selectedVc: {} as VC,
createdVp: null as VC,
reason: '',
loggers: [] as EmitterSubscription[],
vcName: '',
@@ -88,6 +90,11 @@ export const scanMachine = model.createMachine(
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
services: {} as {
createVp: {
data: VC;
};
},
},
id: 'scan',
initial: 'inactive',
@@ -261,7 +268,7 @@ export const scanMachine = model.createMachine(
actions: ['setSelectedVc'],
},
VERIFY_AND_SELECT_VC: {
target: 'verifyingUserIdentity',
target: 'verifyingIdentity',
actions: ['setSelectedVc'],
},
CANCEL: 'idle',
@@ -310,25 +317,38 @@ export const scanMachine = model.createMachine(
},
rejected: {},
navigatingToHome: {},
verifyingUserIdentity: {
verifyingIdentity: {
on: {
FACE_VALID: {
target: 'sendingVc',
target: 'creatingVp',
},
FACE_INVALID: {
target: 'invalidUserIdentity',
target: 'invalidIdentity',
},
CANCEL: 'selectingVc',
},
},
invalidUserIdentity: {
creatingVp: {
invoke: {
src: 'createVp',
onDone: {
actions: 'setCreatedVp',
target: 'sendingVc',
},
onError: {
actions: log('Could not create Verifiable Presentation'),
target: 'selectingVc',
},
},
},
invalidIdentity: {
on: {
DISMISS: 'selectingVc',
RETRY_VERIFICATION: 'verifyingUserIdentity',
RETRY_VERIFICATION: 'verifyingIdentity',
},
},
},
exit: ['disconnect', 'clearReason'],
exit: ['disconnect', 'clearReason', 'clearCreatedVp'],
},
disconnected: {
id: 'disconnected',
@@ -348,7 +368,7 @@ export const scanMachine = model.createMachine(
requestSenderInfo: sendParent('REQUEST_DEVICE_INFO'),
setSenderInfo: model.assign({
senderInfo: (_, event) => event.info,
senderInfo: (_context, event) => event.info,
}),
requestToEnableLocation: () => requestLocation(),
@@ -380,16 +400,16 @@ export const scanMachine = model.createMachine(
}),
setReceiverInfo: model.assign({
receiverInfo: (_, event) => event.receiverInfo,
receiverInfo: (_context, event) => event.receiverInfo,
}),
setReason: model.assign({
reason: (_, event) => event.reason,
reason: (_context, event) => event.reason,
}),
clearReason: assign({ reason: '' }),
setSelectedVc: model.assign({
setSelectedVc: assign({
selectedVc: (context, event) => {
const reason = [];
if (context.reason.trim() !== '') {
@@ -399,6 +419,14 @@ export const scanMachine = model.createMachine(
},
}),
setCreatedVp: assign({
createdVp: (_context, event) => event.data,
}),
clearCreatedVp: assign({
createdVp: () => null,
}),
registerLoggers: assign({
loggers: (context) => {
if (context.sharingProtocol === 'OFFLINE' && __DEV__) {
@@ -558,7 +586,12 @@ export const scanMachine = model.createMachine(
sendVc: (context) => (callback) => {
let subscription: EmitterSubscription;
const vc = { ...context.selectedVc, tag: '' };
const vp = context.createdVp;
const vc = {
...(vp != null ? vp : context.selectedVc),
tag: '',
};
const statusCallback = (status: SendVcStatus) => {
if (typeof status === 'number') return;
callback({
@@ -588,6 +621,26 @@ export const scanMachine = model.createMachine(
});
}
},
createVp: (context) => async () => {
// TODO
// const verifiablePresentation = await createVerifiablePresentation(...);
const verifiablePresentation: VerifiablePresentation = {
'@context': [''],
'proof': null,
'type': 'VerifiablePresentation',
'verifiableCredential': [context.selectedVc.verifiableCredential],
};
const vc: VC = {
...context.selectedVc,
verifiableCredential: null,
verifiablePresentation,
};
return Promise.resolve(vc);
},
},
guards: {
@@ -708,12 +761,12 @@ export function selectIsDone(state: State) {
return state.matches('reviewing.navigatingToHome');
}
export function selectIsVerifyingUserIdentity(state: State) {
return state.matches('reviewing.verifyingUserIdentity');
export function selectIsVerifyingIdentity(state: State) {
return state.matches('reviewing.verifyingIdentity');
}
export function selectIsInvalidUserIdentity(state: State) {
return state.matches('reviewing.invalidUserIdentity');
export function selectIsInvalidIdentity(state: State) {
return state.matches('reviewing.invalidIdentity');
}
export function selectIsCancelling(state: State) {

View File

@@ -3,6 +3,15 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
'done.invoke.scan.reviewing.creatingVp:invocation[0]': {
type: 'done.invoke.scan.reviewing.creatingVp:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.scan.reviewing.creatingVp:invocation[0]': {
type: 'error.platform.scan.reviewing.creatingVp:invocation[0]';
data: unknown;
};
'xstate.after(3000)#scan.reviewing.cancelling': {
type: 'xstate.after(3000)#scan.reviewing.cancelling';
};
@@ -15,6 +24,7 @@ export interface Typegen0 {
'invokeSrcNameMap': {
checkLocationPermission: 'done.invoke.scan.checkingLocationService.checkingPermission:invocation[0]';
checkLocationStatus: 'done.invoke.checkingLocationService:invocation[0]';
createVp: 'done.invoke.scan.reviewing.creatingVp:invocation[0]';
discoverDevice: 'done.invoke.scan.connecting:invocation[0]';
exchangeDeviceInfo: 'done.invoke.scan.exchangingDeviceInfo:invocation[0]';
monitorConnection: 'done.invoke.scan:invocation[0]';
@@ -28,6 +38,14 @@ export interface Typegen0 {
delays: never;
};
'eventsCausingActions': {
clearCreatedVp:
| 'CANCEL'
| 'DISCONNECT'
| 'DISMISS'
| 'SCREEN_BLUR'
| 'SCREEN_FOCUS'
| 'xstate.after(3000)#scan.reviewing.cancelling'
| 'xstate.stop';
clearReason:
| 'CANCEL'
| 'DISCONNECT'
@@ -71,6 +89,7 @@ export interface Typegen0 {
requestSenderInfo: 'SCAN';
requestToEnableLocation: 'LOCATION_DISABLED' | 'LOCATION_REQUEST';
setConnectionParams: 'SCAN';
setCreatedVp: 'done.invoke.scan.reviewing.creatingVp:invocation[0]';
setReason: 'UPDATE_REASON';
setReceiverInfo: 'EXCHANGE_DONE';
setScannedQrParams: 'SCAN';
@@ -80,11 +99,12 @@ export interface Typegen0 {
'eventsCausingServices': {
checkLocationPermission: 'APP_ACTIVE' | 'LOCATION_ENABLED';
checkLocationStatus: 'CANCEL' | 'SCREEN_FOCUS';
createVp: 'FACE_VALID';
discoverDevice: 'RECEIVE_DEVICE_INFO';
exchangeDeviceInfo: 'CONNECTED';
monitorConnection: 'SCREEN_BLUR' | 'SCREEN_FOCUS' | 'xstate.init';
sendDisconnect: 'CANCEL';
sendVc: 'FACE_VALID' | 'SELECT_VC';
sendVc: 'SELECT_VC' | 'done.invoke.scan.reviewing.creatingVp:invocation[0]';
};
'eventsCausingGuards': {
isQrOffline: 'SCAN';
@@ -94,9 +114,9 @@ export interface Typegen0 {
CLEAR_DELAY: 'LOCATION_ENABLED';
CONNECTION_TIMEOUT:
| 'CONNECTED'
| 'FACE_VALID'
| 'RECEIVE_DEVICE_INFO'
| 'SELECT_VC';
| 'SELECT_VC'
| 'done.invoke.scan.reviewing.creatingVp:invocation[0]';
};
'matchesStates':
| 'checkingLocationService'
@@ -120,15 +140,16 @@ export interface Typegen0 {
| 'reviewing'
| 'reviewing.accepted'
| 'reviewing.cancelling'
| 'reviewing.creatingVp'
| 'reviewing.idle'
| 'reviewing.invalidUserIdentity'
| 'reviewing.invalidIdentity'
| 'reviewing.navigatingToHome'
| 'reviewing.rejected'
| 'reviewing.selectingVc'
| 'reviewing.sendingVc'
| 'reviewing.sendingVc.inProgress'
| 'reviewing.sendingVc.timeout'
| 'reviewing.verifyingUserIdentity'
| 'reviewing.verifyingIdentity'
| {
checkingLocationService?:
| 'checkingPermission'
@@ -141,13 +162,14 @@ export interface Typegen0 {
reviewing?:
| 'accepted'
| 'cancelling'
| 'creatingVp'
| 'idle'
| 'invalidUserIdentity'
| 'invalidIdentity'
| 'navigatingToHome'
| 'rejected'
| 'selectingVc'
| 'sendingVc'
| 'verifyingUserIdentity'
| 'verifyingIdentity'
| { sendingVc?: 'inProgress' | 'timeout' };
};
'tags': never;

View File

@@ -1,5 +1,6 @@
{
"header": "{{vcLabel}} details",
"acceptRequest": "Accept request and receive {{vcLabel}}",
"acceptRequestAndVerify": "Accept request and verify",
"reject": "Reject"
}

View File

@@ -1,37 +1,82 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { DeviceInfoList } from '../../components/DeviceInfoList';
import { Button, Column, Text } from '../../components/ui';
import { Button, Column, Row, Text } from '../../components/ui';
import { Colors } from '../../components/ui/styleUtils';
import { VcDetails } from '../../components/VcDetails';
import { useReceiveVcScreen } from './ReceiveVcScreenController';
import { useTranslation } from 'react-i18next';
import { VerifyIdentityOverlay } from '../VerifyIdentityOverlay';
import { MessageOverlay } from '../../components/MessageOverlay';
export const ReceiveVcScreen: React.FC = () => {
const { t } = useTranslation('ReceiveVcScreen');
const controller = useReceiveVcScreen();
return (
<Column scroll padding="24 0 48 0" backgroundColor={Colors.LightGrey}>
<Column>
<DeviceInfoList of="sender" deviceInfo={controller.senderInfo} />
<Text weight="semibold" margin="24 24 0 24">
{t('header', { vcLabel: controller.vcLabel.singular })}
</Text>
<VcDetails vc={controller.incomingVc} />
<React.Fragment>
<Column scroll padding="24 0 48 0" backgroundColor={Colors.LightGrey}>
<Column>
<DeviceInfoList of="sender" deviceInfo={controller.senderInfo} />
<Text weight="semibold" margin="24 24 0 24">
{t('header', { vcLabel: controller.vcLabel.singular })}
</Text>
<VcDetails vc={controller.incomingVc} />
</Column>
<Column padding="0 24" margin="32 0 0 0">
{controller.isIncomingVp ? (
<Button
type="outline"
title={t('acceptRequestAndVerify')}
margin="12 0 12 0"
onPress={controller.ACCEPT_AND_VERIFY}
/>
) : (
<Button
title={t('acceptRequest', {
vcLabel: controller.vcLabel.singular,
})}
margin="12 0 12 0"
onPress={controller.ACCEPT}
/>
)}
<Button
type="clear"
title={t('reject')}
margin="0 0 12 0"
onPress={controller.REJECT}
/>
</Column>
</Column>
<Column padding="0 24" margin="32 0 0 0">
<Button
title={t('acceptRequest', { vcLabel: controller.vcLabel.singular })}
margin="12 0 12 0"
onPress={controller.ACCEPT}
/>
<Button
type="clear"
title={t('reject')}
margin="0 0 12 0"
onPress={controller.REJECT}
/>
</Column>
</Column>
<VerifyIdentityOverlay
vc={controller.incomingVc}
isVisible={controller.isVerifyingIdentity}
onCancel={controller.CANCEL}
onFaceValid={controller.FACE_VALID}
onFaceInvalid={controller.FACE_INVALID}
/>
<MessageOverlay
isVisible={controller.isInvalidIdentity}
title={t('VerifyIdentityOverlay:errors.invalidIdentity.title')}
message={t('VerifyIdentityOverlay:errors.invalidIdentity.message')}
onBackdropPress={controller.DISMISS}>
<Row>
<Button
fill
type="clear"
title={t('common:cancel')}
onPress={controller.DISMISS}
margin={[0, 8, 0, 0]}
/>
<Button
fill
title={t('common:tryAgain')}
onPress={controller.RETRY_VERIFICATION}
/>
</Row>
</MessageOverlay>
</React.Fragment>
);
};

View File

@@ -3,6 +3,9 @@ import { useContext } from 'react';
import {
RequestEvents,
selectIncomingVc,
selectIsIncomingVp,
selectIsInvalidIdentity,
selectIsVerifyingIdentity,
selectSenderInfo,
} from '../../machines/request';
import { selectVcLabel } from '../../machines/settings';
@@ -18,7 +21,19 @@ export function useReceiveVcScreen() {
incomingVc: useSelector(requestService, selectIncomingVc),
vcLabel: useSelector(settingsService, selectVcLabel),
isIncomingVp: useSelector(requestService, selectIsIncomingVp),
isVerifyingIdentity: useSelector(requestService, selectIsVerifyingIdentity),
isInvalidIdentity: useSelector(requestService, selectIsInvalidIdentity),
ACCEPT: () => requestService.send(RequestEvents.ACCEPT()),
ACCEPT_AND_VERIFY: () =>
requestService.send(RequestEvents.ACCEPT_AND_VERIFY()),
REJECT: () => requestService.send(RequestEvents.REJECT()),
RETRY_VERIFICATION: () =>
requestService.send(RequestEvents.RETRY_VERIFICATION()),
CANCEL: () => requestService.send(RequestEvents.CANCEL()),
DISMISS: () => requestService.send(RequestEvents.DISMISS()),
FACE_VALID: () => requestService.send(RequestEvents.FACE_VALID()),
FACE_INVALID: () => requestService.send(RequestEvents.FACE_INVALID()),
};
}

View File

@@ -14,13 +14,6 @@
"rejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
},
"verifyingIdentity": "Verifying identity..."
},
"errors": {
"invalidIdentity": {
"title": "Unable to verify identity",
"message": "An error occured and we couldn't scan your portrait. Try again, make sure your face is visible, devoid of any accessories."
}
}
}

View File

@@ -1,13 +1,14 @@
import React from 'react';
import { Input } from 'react-native-elements';
import { useTranslation } from 'react-i18next';
import { DeviceInfoList } from '../../components/DeviceInfoList';
import { Button, Column, Row } from '../../components/ui';
import { Colors } from '../../components/ui/styleUtils';
import { SelectVcOverlay } from './SelectVcOverlay';
import { MessageOverlay } from '../../components/MessageOverlay';
import { useSendVcScreen } from './SendVcScreenController';
import { useTranslation } from 'react-i18next';
import { VerifyIdentityOverlay } from './VerifyIdentityOverlay';
import { VerifyIdentityOverlay } from '../VerifyIdentityOverlay';
export const SendVcScreen: React.FC = () => {
const { t } = useTranslation('SendVcScreen');
@@ -58,16 +59,17 @@ export const SendVcScreen: React.FC = () => {
/>
<VerifyIdentityOverlay
isVisible={controller.isVerifyingUserIdentity}
isVisible={controller.isVerifyingIdentity}
vc={controller.selectedVc}
onCancel={controller.CANCEL}
onFaceValid={controller.FACE_VALID}
onFaceInvalid={controller.FACE_INVALID}
/>
<MessageOverlay
isVisible={controller.isInvalidUserIdentity}
title={t('errors.invalidIdentity.title')}
message={t('errors.invalidIdentity.message')}
isVisible={controller.isInvalidIdentity}
title={t('VerifyIdentityOverlay:errors.invalidIdentity.title')}
message={t('VerifyIdentityOverlay:errors.invalidIdentity.message')}
onBackdropPress={controller.DISMISS}>
<Row>
<Button

View File

@@ -12,8 +12,9 @@ import {
selectIsSendingVc,
selectVcName,
selectIsSendingVcTimeout,
selectIsVerifyingUserIdentity,
selectIsInvalidUserIdentity,
selectIsVerifyingIdentity,
selectIsInvalidIdentity,
selectSelectedVc,
selectIsCancelling,
} from '../../machines/scan';
import { selectVcLabel } from '../../machines/settings';
@@ -52,20 +53,15 @@ export function useSendVcScreen() {
vcName: useSelector(scanService, selectVcName),
vcLabel: useSelector(settingsService, selectVcLabel),
vcKeys: useSelector(vcService, selectShareableVcs),
selectedVc: useSelector(scanService, selectSelectedVc),
isSelectingVc: useSelector(scanService, selectIsSelectingVc),
isSendingVc,
isSendingVcTimeout,
isAccepted: useSelector(scanService, selectIsAccepted),
isRejected: useSelector(scanService, selectIsRejected),
isVerifyingUserIdentity: useSelector(
scanService,
selectIsVerifyingUserIdentity
),
isInvalidUserIdentity: useSelector(
scanService,
selectIsInvalidUserIdentity
),
isVerifyingIdentity: useSelector(scanService, selectIsVerifyingIdentity),
isInvalidIdentity: useSelector(scanService, selectIsInvalidIdentity),
isCancelling: useSelector(scanService, selectIsCancelling),
ACCEPT_REQUEST: () => scanService.send(ScanEvents.ACCEPT_REQUEST()),

View File

@@ -1,21 +0,0 @@
import { useContext } from 'react';
import { scanMachine, selectSelectedVc } from '../../machines/scan';
import { GlobalContext } from '../../shared/GlobalContext';
import { useSelector } from '@xstate/react';
export function useVerifyIdentityOverlay() {
const { appService } = useContext(GlobalContext);
const scanService = appService.children.get(scanMachine.id);
return {
selectedVc: useSelector(scanService, selectSelectedVc),
};
}
export interface VerifyIdentityOverlayProps {
isVisible: boolean;
onCancel: () => void;
onFaceValid: () => void;
onFaceInvalid: () => void;
}

View File

@@ -0,0 +1,11 @@
{
"status": {
"verifyingIdentity": "Verifying identity..."
},
"errors": {
"invalidIdentity": {
"title": "Unable to verify identity",
"message": "An error occured and we couldn't scan your portrait. Try again, make sure your face is visible, devoid of any accessories."
}
}
}

View File

@@ -1,13 +1,10 @@
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { Icon, Overlay } from 'react-native-elements';
import { FaceScanner } from '../../components/FaceScanner';
import { Column, Row } from '../../components/ui';
import { Colors } from '../../components/ui/styleUtils';
import {
useVerifyIdentityOverlay,
VerifyIdentityOverlayProps,
} from './VerifyIdentityOverlayController';
import { FaceScanner } from '../components/FaceScanner';
import { Column, Row } from '../components/ui';
import { Colors } from '../components/ui/styleUtils';
import { VC } from '../types/vc';
const styles = StyleSheet.create({
content: {
@@ -20,17 +17,15 @@ const styles = StyleSheet.create({
export const VerifyIdentityOverlay: React.FC<VerifyIdentityOverlayProps> = (
props
) => {
const controller = useVerifyIdentityOverlay();
return (
<Overlay isVisible={props.isVisible}>
<Row align="flex-end" padding="16">
<Icon name="close" color={Colors.Orange} onPress={props.onCancel} />
</Row>
<Column fill style={styles.content} align="center">
{controller.selectedVc?.credential != null && (
{props.vc?.credential != null && (
<FaceScanner
vcImage={controller.selectedVc.credential.biometrics.face}
vcImage={props.vc.credential.biometrics.face}
onValid={props.onFaceValid}
onInvalid={props.onFaceInvalid}
/>
@@ -39,3 +34,11 @@ export const VerifyIdentityOverlay: React.FC<VerifyIdentityOverlayProps> = (
</Overlay>
);
};
export interface VerifyIdentityOverlayProps {
isVisible: boolean;
vc?: VC;
onCancel: () => void;
onFaceValid: () => void;
onFaceInvalid: () => void;
}

View File

@@ -2,5 +2,11 @@
"cancel": "Cancel",
"save": "Save",
"editLabel": "Edit {{label}}",
"tryAgain": "Try again"
"tryAgain": "Try again",
"camera": {
"errors": {
"missingPermission": "This app uses the camera to scan the QR code of another device."
},
"allowAccess": "Allow access to camera"
}
}

View File

@@ -4,6 +4,7 @@ export interface VC {
tag: string;
credential: DecodedCredential;
verifiableCredential: VerifiableCredential;
verifiablePresentation?: VerifiablePresentation;
generatedOn: Date;
requestId: string;
isVerified: boolean;