mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-09 05:27:57 -05:00
feat: verify identity on both sides
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"header": "{{vcLabel}} details",
|
||||
"acceptRequest": "Accept request and receive {{vcLabel}}",
|
||||
"acceptRequestAndVerify": "Accept request and verify",
|
||||
"reject": "Reject"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
11
screens/VerifyIdentityOverlay.strings.json
Normal file
11
screens/VerifyIdentityOverlay.strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ export interface VC {
|
||||
tag: string;
|
||||
credential: DecodedCredential;
|
||||
verifiableCredential: VerifiableCredential;
|
||||
verifiablePresentation?: VerifiablePresentation;
|
||||
generatedOn: Date;
|
||||
requestId: string;
|
||||
isVerified: boolean;
|
||||
|
||||
Reference in New Issue
Block a user