Files
inji-wallet/machines/VCItemMachine/EsignetMosipVCItem/EsignetMosipVCItemMachine.ts
Alka Prasad 9b07e63383 [Inji 434 & INJI 441]: Add events in receiver VC sharing flow and cancel button handle (#990)
* refactor(INJI-434): rename some variables and functions to meaning names

Signed-off-by: Alka <prasadalka1998@gmail.com>

* feat(INJI-434): add telemetry events in the VC share flow on verifier end

Signed-off-by: Alka <prasadalka1998@gmail.com>

* fix(INJI-434): handle cancel button press during vc share

Signed-off-by: Alka <prasadalka1998@gmail.com>

* fix(INJI-441): handle cancel button press during vc share

Signed-off-by: Alka <prasadalka1998@gmail.com>

* refactor(INJI-434): extracts telemetry constants to a seperate file

Signed-off-by: Alka <prasadalka1998@gmail.com>

* refactor(INJI-434): remove unused imports

Signed-off-by: Alka <prasadalka1998@gmail.com>

* fix(INJI-441): update the CANCEL event on press of cancel button

Signed-off-by: Alka <prasadalka1998@gmail.com>

* refactor(INJI-434): update the method name

Signed-off-by: Alka <prasadalka1998@gmail.com>

---------

Signed-off-by: Alka <prasadalka1998@gmail.com>
Signed-off-by: Alka Prasad <Alka1703@users.noreply.github.com>
2023-11-07 10:43:39 +05:30

928 lines
26 KiB
TypeScript

import {assign, ErrorPlatformEvent, EventFrom, send, StateFrom} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {AppServices} from '../../../shared/GlobalContext';
import {VCMetadata} from '../../../shared/VCMetadata';
import {VC} from '../../../types/VC/ExistingMosipVC/vc';
import {
generateKeys,
isHardwareKeystoreExists,
WalletBindingResponse,
} from '../../../shared/cryptoutil/cryptoUtil';
import {log} from 'xstate/lib/actions';
import {Protocols} from '../../../shared/openId4VCI/Utils';
import {StoreEvents} from '../../../machines/store';
import {MIMOTO_BASE_URL, MY_VCS_STORE_KEY} from '../../../shared/constants';
import {VcEvents} from '../../../machines/vc';
import i18n from '../../../i18n';
import {KeyPair} from 'react-native-rsa-native';
import {
getBindingCertificateConstant,
savePrivateKey,
} from '../../../shared/keystore/SecureKeystore';
import {ActivityLogEvents} from '../../../machines/activityLog';
import {request} from '../../../shared/request';
import SecureKeystore from 'react-native-secure-keystore';
import {VerifiableCredential} from './vc';
import {
getEndEventData,
getInteractEventData,
getStartEventData,
sendEndEvent,
sendInteractEvent,
sendStartEvent,
} from '../../../shared/telemetry/TelemetryUtils';
import {TelemetryConstants} from '../../../shared/telemetry/TelemetryConstants';
import {API_URLS} from '../../../shared/api';
const model = createModel(
{
serviceRefs: {} as AppServices,
vcMetadata: {} as VCMetadata,
generatedOn: new Date() as Date,
verifiableCredential: null as VerifiableCredential,
isPinned: false,
hashedId: '',
publicKey: '',
privateKey: '',
otp: '',
otpError: '',
idError: '',
transactionId: '',
bindingTransactionId: '',
walletBindingResponse: null as WalletBindingResponse,
walletBindingError: '',
walletBindingSuccess: false,
isMachineInKebabPopupState: false,
bindingAuthFailedMessage: '' as string,
},
{
events: {
KEY_RECEIVED: (key: string) => ({key}),
KEY_ERROR: (error: Error) => ({error}),
STORE_READY: () => ({}),
DISMISS: () => ({}),
CREDENTIAL_DOWNLOADED: (vc: VC) => ({vc}),
STORE_RESPONSE: (response: VC) => ({response}),
POLL: () => ({}),
DOWNLOAD_READY: () => ({}),
GET_VC_RESPONSE: (vc: VC) => ({vc}),
VERIFY: () => ({}),
LOCK_VC: () => ({}),
RESEND_OTP: () => ({}),
INPUT_OTP: (otp: string) => ({otp}),
REFRESH: () => ({}),
REVOKE_VC: () => ({}),
ADD_WALLET_BINDING_ID: () => ({}),
CANCEL: () => ({}),
CONFIRM: () => ({}),
STORE_ERROR: (error: Error) => ({error}),
PIN_CARD: () => ({}),
KEBAB_POPUP: () => ({}),
SHOW_ACTIVITY: () => ({}),
REMOVE: (vcMetadata: VCMetadata) => ({vcMetadata}),
UPDATE_VC_METADATA: (vcMetadata: VCMetadata) => ({vcMetadata}),
},
},
);
export const EsignetMosipVCItemEvents = model.events;
export const EsignetMosipVCItemMachine = model.createMachine(
{
predictableActionArguments: true,
preserveActionOrder: true,
tsTypes: {} as import('./EsignetMosipVCItemMachine.typegen').Typegen0,
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
},
on: {
REFRESH: {
target: '.checkingStore',
},
UPDATE_VC_METADATA: {
actions: 'setVcMetadata',
},
},
description: 'VC',
id: 'vc-item-openid4vci',
initial: 'checkingVc',
states: {
checkingVc: {
entry: ['requestVcContext'],
description: 'Fetch the VC data from the Memory.',
on: {
GET_VC_RESPONSE: [
{
actions: [
'setVerifiableCredential',
'setContext',
'setGeneratedOn',
],
cond: 'hasCredential',
target: 'idle',
},
{
target: 'checkingStore',
},
],
},
},
checkingStore: {
entry: 'requestStoredContext',
description: 'Check if VC data is in secured local storage.',
on: {
STORE_RESPONSE: {
actions: ['setVerifiableCredential', 'setContext', 'updateVc'],
target: 'idle',
},
},
},
showBindingWarning: {
on: {
CONFIRM: {
actions: 'sendActivationStartEvent',
target: 'requestingBindingOtp',
},
CANCEL: {
target: 'idle',
},
},
},
requestingBindingOtp: {
invoke: {
src: 'requestBindingOtp',
onDone: [
{
target: 'acceptingBindingOtp',
},
],
onError: [
{
actions: ['setWalletBindingError', 'logWalletBindingFailure'],
target: 'showingWalletBindingError',
},
],
},
},
showingWalletBindingError: {
on: {
CANCEL: {
target: 'idle',
actions: 'setWalletBindingErrorEmpty',
},
},
},
acceptingBindingOtp: {
entry: ['clearOtp'],
on: {
INPUT_OTP: {
target: 'addKeyPair',
actions: ['setOtp'],
},
DISMISS: {
target: 'idle',
actions: [
'sendActivationFailedEndEvent',
'clearOtp',
'clearTransactionId',
],
},
RESEND_OTP: {
target: '.resendOTP',
},
},
initial: 'idle',
states: {
idle: {},
resendOTP: {
invoke: {
src: 'requestOtp',
onDone: [
{
target: 'idle',
},
],
},
},
},
},
addKeyPair: {
invoke: {
src: 'generateKeyPair',
onDone: [
{
cond: 'isCustomSecureKeystore',
target: 'addingWalletBindingId',
actions: ['setPublicKey'],
},
{
target: 'addingWalletBindingId',
actions: ['setPublicKey', 'setPrivateKey'],
},
],
onError: [
{
actions: ['setWalletBindingError', 'logWalletBindingFailure'],
target: 'showingWalletBindingError',
},
],
},
},
addingWalletBindingId: {
invoke: {
src: 'addWalletBindnigId',
onDone: [
{
cond: 'isCustomSecureKeystore',
target: 'idle',
actions: [
'setWalletBindingId',
'setThumbprintForWalletBindingId',
'storeContext',
'updateVc',
'setWalletBindingErrorEmpty',
'sendActivationSuccessEvent',
'logWalletBindingSuccess',
],
},
{
target: 'updatingPrivateKey',
actions: [
'setWalletBindingId',
'setThumbprintForWalletBindingId',
],
},
],
onError: [
{
actions: ['setWalletBindingError', 'logWalletBindingFailure'],
target: 'showingWalletBindingError',
},
],
},
},
updatingPrivateKey: {
invoke: {
src: 'updatePrivateKey',
onDone: {
actions: [
'storeContext',
'updatePrivateKey',
'updateVc',
'setWalletBindingErrorEmpty',
'setWalletBindingSuccess',
'logWalletBindingSuccess',
'sendActivationSuccessEvent',
],
target: 'idle',
},
onError: {
actions: ['setWalletBindingError', 'logWalletBindingFailure'],
target: 'showingWalletBindingError',
},
},
},
idle: {
on: {
DISMISS: {
target: 'checkingVc',
actions: 'resetWalletBindingSuccess',
},
KEBAB_POPUP: {
target: 'kebabPopUp',
},
ADD_WALLET_BINDING_ID: {
target: 'showBindingWarning',
},
PIN_CARD: {
target: 'pinCard',
actions: 'setPinCard',
},
},
},
pinCard: {
entry: 'storeContext',
on: {
STORE_RESPONSE: {
actions: ['sendVcUpdated', 'VcUpdated'],
target: 'idle',
},
},
},
kebabPopUp: {
entry: assign({
isMachineInKebabPopupState: () => {
return true;
},
}),
on: {
DISMISS: {
actions: assign({
isMachineInKebabPopupState: () => false,
}),
target: 'idle',
},
ADD_WALLET_BINDING_ID: {
target: '#vc-item-openid4vci.kebabPopUp.showBindingWarning',
},
PIN_CARD: {
target: '#vc-item-openid4vci.pinCard',
actions: 'setPinCard',
},
SHOW_ACTIVITY: {
target: '#vc-item-openid4vci.kebabPopUp.showActivities',
},
REMOVE: {
actions: 'setVcKey',
target: '#vc-item-openid4vci.kebabPopUp.removeWallet',
},
},
initial: 'idle',
states: {
idle: {},
showBindingWarning: {
on: {
CONFIRM: {
actions: 'sendActivationStartEvent',
target: '#vc-item-openid4vci.kebabPopUp.requestingBindingOtp',
},
CANCEL: {
target: '#vc-item-openid4vci.kebabPopUp',
},
},
},
requestingBindingOtp: {
invoke: {
src: 'requestBindingOtp',
onDone: [
{
target: '#vc-item-openid4vci.kebabPopUp.acceptingBindingOtp',
actions: [log('accceptingOTP')],
},
],
onError: [
{
actions: 'setWalletBindingError',
target:
'#vc-item-openid4vci.kebabPopUp.showingWalletBindingError',
},
],
},
},
showingWalletBindingError: {
on: {
CANCEL: {
target: '#vc-item-openid4vci.kebabPopUp',
actions: 'setWalletBindingErrorEmpty',
},
},
},
acceptingBindingOtp: {
entry: ['clearOtp'],
on: {
INPUT_OTP: {
target: '#vc-item-openid4vci.kebabPopUp.addKeyPair',
actions: ['setOtp'],
},
DISMISS: {
target: '#vc-item-openid4vci.kebabPopUp',
actions: [
'sendActivationFailedEndEvent',
'clearOtp',
'clearTransactionId',
],
},
RESEND_OTP: {
target: '.resendOTP',
},
},
initial: 'idle',
states: {
idle: {},
resendOTP: {
invoke: {
src: 'requestOtp',
onDone: [
{
target: 'idle',
},
],
},
},
},
},
addKeyPair: {
invoke: {
src: 'generateKeyPair',
onDone: [
{
cond: 'isCustomSecureKeystore',
target:
'#vc-item-openid4vci.kebabPopUp.addingWalletBindingId',
actions: ['setPublicKey'],
},
{
target:
'#vc-item-openid4vci.kebabPopUp.addingWalletBindingId',
actions: ['setPublicKey', 'setPrivateKey'],
},
],
onError: [
{
actions: 'setWalletBindingError',
target:
'#vc-item-openid4vci.kebabPopUp.showingWalletBindingError',
},
],
},
},
addingWalletBindingId: {
invoke: {
src: 'addWalletBindnigId',
onDone: [
{
cond: 'isCustomSecureKeystore',
target: '#vc-item-openid4vci.kebabPopUp',
actions: [
'setWalletBindingId',
'setThumbprintForWalletBindingId',
'storeContext',
'updateVc',
'setWalletBindingErrorEmpty',
'sendWalletBindingSuccess',
'sendActivationSuccessEvent',
'logWalletBindingSuccess',
],
},
{
target: '#vc-item-openid4vci.kebabPopUp.updatingPrivateKey',
actions: [
'setWalletBindingId',
'setThumbprintForWalletBindingId',
],
},
],
onError: [
{
actions: ['setWalletBindingError', 'logWalletBindingFailure'],
target:
'#vc-item-openid4vci.kebabPopUp.showingWalletBindingError',
},
],
},
},
updatingPrivateKey: {
invoke: {
src: 'updatePrivateKey',
onDone: {
actions: [
'storeContext',
'updatePrivateKey',
'updateVc',
'setWalletBindingErrorEmpty',
'sendWalletBindingSuccess',
'sendActivationSuccessEvent',
'logWalletBindingSuccess',
],
target: '#vc-item-openid4vci.kebabPopUp',
},
onError: {
actions: 'setWalletBindingError',
target:
'#vc-item-openid4vci.kebabPopUp.showingWalletBindingError',
},
},
},
showActivities: {
on: {
DISMISS: '#vc-item-openid4vci.kebabPopUp',
},
},
removeWallet: {
on: {
CONFIRM: {
target: 'removingVc',
},
CANCEL: {
target: 'idle',
},
},
},
removingVc: {
entry: 'removeVcItem',
on: {
STORE_RESPONSE: {
actions: ['removedVc', 'logVCremoved'],
target: '#vc-item-openid4vci',
},
},
},
},
},
},
},
{
actions: {
setVcMetadata: assign({
vcMetadata: (_, event) => event.vcMetadata,
}),
requestVcContext: send(
context => ({
type: 'GET_VC_ITEM',
vcMetadata: context.vcMetadata,
protocol: Protocols.OpenId4VCI,
}),
{
to: context => context.serviceRefs.vc,
},
),
requestStoredContext: send(
context => {
return StoreEvents.GET(
VCMetadata.fromVC(context.vcMetadata).getVcKey(),
);
},
{
to: context => context.serviceRefs.store,
},
),
updateVc: send(
context => {
const {serviceRefs, ...verifiableCredential} = context;
return {
type: 'VC_DOWNLOADED_FROM_OPENID4VCI',
vc: verifiableCredential,
vcMetadata: context.vcMetadata,
};
},
{
to: context => context.serviceRefs.vc,
},
),
setVerifiableCredential: model.assign({
verifiableCredential: (_, event) => {
if (event.type === 'GET_VC_RESPONSE') {
return event.vc.verifiableCredential
? event.vc.verifiableCredential
: event.vc;
}
return event.response.verifiableCredential;
},
}),
setContext: model.assign((context, event) => {
if (event.type === 'STORE_RESPONSE') {
const {verifiableCredential, ...data} = event.response;
return {...context, ...data};
}
if (event.type === 'GET_VC_RESPONSE') {
const {verifiableCredential, ...data} = event.vc;
return {...context, ...data};
}
return context;
}),
setGeneratedOn: model.assign({
generatedOn: (_context, event) => {
if (event.type === 'GET_VC_RESPONSE') {
return event.vc.generatedOn;
}
return event.response.generatedOn;
},
}),
storeContext: send(
context => {
const {serviceRefs, ...data} = context;
data.credentialRegistry = MIMOTO_BASE_URL;
return StoreEvents.SET(
VCMetadata.fromVC(context.vcMetadata).getVcKey(),
data,
);
},
{
to: context => context.serviceRefs.store,
},
),
setPinCard: assign(context => {
return {
...context,
isPinned: !context.isPinned,
};
}),
VcUpdated: send(
context => {
const {serviceRefs, ...vc} = context;
return {type: 'VC_UPDATE', vc};
},
{
to: context => context.serviceRefs.vc,
},
),
sendVcUpdated: send(
context =>
VcEvents.VC_METADATA_UPDATED(
new VCMetadata({...context.vcMetadata, isPinned: context.isPinned}),
),
{
to: context => context.serviceRefs.vc,
},
),
setWalletBindingError: assign({
walletBindingError: () =>
i18n.t(`errors.genericError`, {
ns: 'common',
}),
bindingAuthFailedMessage: (_context, event) => {
const error = JSON.parse(JSON.stringify(event.data)).name;
if (error) {
return error;
}
return '';
},
}),
setWalletBindingErrorEmpty: assign({
walletBindingError: () => '',
bindingAuthFailedMessage: () => '',
}),
setWalletBindingSuccess: assign({
walletBindingSuccess: true,
}),
resetWalletBindingSuccess: assign({
walletBindingSuccess: false,
}),
sendWalletBindingSuccess: send(
context => {
return {
type: 'WALLET_BINDING_SUCCESS',
};
},
{
to: context => context.serviceRefs.vc,
},
),
sendActivationStartEvent: context => {
sendStartEvent(
getStartEventData(
context.isMachineInKebabPopupState
? TelemetryConstants.FlowType.vcActivationFromKebab
: TelemetryConstants.FlowType.vcActivation,
),
);
sendInteractEvent(
getInteractEventData(
context.isMachineInKebabPopupState
? TelemetryConstants.FlowType.vcActivationFromKebab
: TelemetryConstants.FlowType.vcActivation,
TelemetryConstants.InteractEventSubtype.click,
'Activate Button',
),
);
},
sendActivationFailedEndEvent: context =>
sendEndEvent(
getEndEventData(
context.isMachineInKebabPopupState
? TelemetryConstants.FlowType.vcActivationFromKebab
: TelemetryConstants.FlowType.vcActivation,
TelemetryConstants.EndEventStatus.failure,
{
errorId: TelemetryConstants.ErrorId.userCancel,
errorMessage: TelemetryConstants.ErrorMessage.activationCancelled,
},
),
),
sendActivationSuccessEvent: context =>
sendEndEvent(
getEndEventData(
context.isMachineInKebabPopupState
? TelemetryConstants.FlowType.vcActivationFromKebab
: TelemetryConstants.FlowType.vcActivation,
TelemetryConstants.EndEventStatus.success,
),
),
setPublicKey: assign({
publicKey: (context, event) => {
if (!isHardwareKeystoreExists) {
return (event.data as KeyPair).public;
}
return event.data as string;
},
}),
setPrivateKey: assign({
privateKey: (context, event) => (event.data as KeyPair).private,
}),
updatePrivateKey: assign({
privateKey: () => '',
}),
setWalletBindingId: assign({
walletBindingResponse: (context, event) =>
event.data as WalletBindingResponse,
}),
setThumbprintForWalletBindingId: send(
context => {
const {walletBindingResponse} = context;
const walletBindingIdKey = getBindingCertificateConstant(
walletBindingResponse.walletBindingId,
);
return StoreEvents.SET(
walletBindingIdKey,
walletBindingResponse.thumbprint,
);
},
{
to: context => context.serviceRefs.store,
},
),
removedVc: send(
() => ({
type: 'REFRESH_MY_VCS',
}),
{
to: context => context.serviceRefs.vc,
},
),
logWalletBindingSuccess: send(
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VCMetadata.fromVC(context.vcMetadata).getVcKey(),
type: 'WALLET_BINDING_SUCCESSFULL',
timestamp: Date.now(),
deviceName: '',
vcLabel: VCMetadata.fromVC(context.vcMetadata).id,
}),
{
to: context => context.serviceRefs.activityLog,
},
),
logWalletBindingFailure: send(
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VCMetadata.fromVC(context.vcMetadata).getVcKey(),
type: 'WALLET_BINDING_FAILURE',
timestamp: Date.now(),
deviceName: '',
vcLabel: VCMetadata.fromVC(context.vcMetadata).id,
}),
{
to: context => context.serviceRefs.activityLog,
},
),
setOtp: model.assign({
otp: (_, event) => event.otp,
}),
setOtpError: assign({
otpError: (_context, event) =>
(event as ErrorPlatformEvent).data.message,
}),
clearOtp: assign({otp: ''}),
removeVcItem: send(
_context => {
return StoreEvents.REMOVE(
MY_VCS_STORE_KEY,
_context.vcMetadata.getVcKey(),
);
},
{to: context => context.serviceRefs.store},
),
setVcKey: model.assign({
vcMetadata: (_, event) => event.vcMetadata,
}),
logVCremoved: send(
(context, _) =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VCMetadata.fromVC(context.vcMetadata).getVcKey(),
type: 'VC_REMOVED',
timestamp: Date.now(),
deviceName: '',
vcLabel: VCMetadata.fromVC(context.vcMetadata).id,
}),
{
to: context => context.serviceRefs.activityLog,
},
),
},
services: {
updatePrivateKey: async context => {
const hasSetPrivateKey: boolean = await savePrivateKey(
context.walletBindingResponse.walletBindingId,
context.privateKey,
);
if (!hasSetPrivateKey) {
throw new Error('Could not store private key in keystore.');
}
return '';
},
addWalletBindnigId: async context => {
const response = await request(
API_URLS.walletBinding.method,
API_URLS.walletBinding.buildURL(),
{
requestTime: String(new Date().toISOString()),
request: {
authFactorType: 'WLA',
format: 'jwt',
individualId: VCMetadata.fromVC(context.vcMetadata).id,
transactionId: context.transactionId,
publicKey: context.publicKey,
challengeList: [
{
authFactorType: 'OTP',
challenge: context.otp,
format: 'alpha-numeric',
},
],
},
},
);
const certificate = response.response.certificate;
await savePrivateKey(
getBindingCertificateConstant(
VCMetadata.fromVC(context.vcMetadata).id,
),
certificate,
);
const walletResponse: WalletBindingResponse = {
walletBindingId: response.response.encryptedWalletBindingId,
keyId: response.response.keyId,
thumbprint: response.response.thumbprint,
expireDateTime: response.response.expireDateTime,
};
return walletResponse;
},
generateKeyPair: async context => {
if (!isHardwareKeystoreExists) {
return await generateKeys();
}
const isBiometricsEnabled = SecureKeystore.hasBiometricsEnabled();
return SecureKeystore.generateKeyPair(
VCMetadata.fromVC(context.vcMetadata).id,
isBiometricsEnabled,
0,
);
},
requestBindingOtp: async context => {
const response = await request(
API_URLS.bindingOtp.method,
API_URLS.bindingOtp.buildURL(),
{
requestTime: String(new Date().toISOString()),
request: {
individualId: context.verifiableCredential.credential
.credentialSubject.VID
? context.verifiableCredential.credential.credentialSubject.VID
: context.verifiableCredential.credential.credentialSubject.UIN,
otpChannels: ['EMAIL', 'PHONE'],
},
},
);
if (response.response == null) {
throw new Error('Could not process request');
}
},
},
guards: {
hasCredential: (_, event) => {
const vc = event.vc;
return vc != null;
},
isCustomSecureKeystore: () => isHardwareKeystoreExists,
},
},
);
export const createEsignetMosipVCItemMachine = (
serviceRefs: AppServices,
vcMetadata: VCMetadata,
) => {
return EsignetMosipVCItemMachine.withContext({
...EsignetMosipVCItemMachine.context,
serviceRefs,
vcMetadata,
});
};