diff --git a/.env b/.env index d055ca21fc..6ff684b9d5 100644 --- a/.env +++ b/.env @@ -21,4 +21,4 @@ DEBUG_MODE=false APPLICATION_LANGUAGE=en #Toggle for openID for VC -ENABLE_OPENID_FOR_VC=false +ENABLE_OPENID_FOR_VC=true diff --git a/machines/activityLog.typegen.ts b/machines/activityLog.typegen.ts index 686854d097..a21e978730 100644 --- a/machines/activityLog.typegen.ts +++ b/machines/activityLog.typegen.ts @@ -2,31 +2,31 @@ export interface Typegen0 { '@@xstate/typegen': true; - 'internalEvents': { - 'xstate.init': { type: 'xstate.init' }; + internalEvents: { + 'xstate.init': {type: 'xstate.init'}; }; - 'invokeSrcNameMap': {}; - 'missingImplementations': { + invokeSrcNameMap: {}; + missingImplementations: { actions: never; delays: never; guards: never; services: never; }; - 'eventsCausingActions': { + eventsCausingActions: { loadActivities: 'REFRESH' | 'xstate.init'; prependActivity: 'STORE_RESPONSE'; setActivities: 'STORE_RESPONSE'; storeActivity: 'LOG_ACTIVITY'; }; - 'eventsCausingDelays': {}; - 'eventsCausingGuards': {}; - 'eventsCausingServices': {}; - 'matchesStates': + eventsCausingDelays: {}; + eventsCausingGuards: {}; + eventsCausingServices: {}; + matchesStates: | 'init' | 'ready' | 'ready.idle' | 'ready.logging' | 'ready.refreshing' - | { ready?: 'idle' | 'logging' | 'refreshing' }; - 'tags': never; + | {ready?: 'idle' | 'logging' | 'refreshing'}; + tags: never; } diff --git a/machines/auth.typegen.ts b/machines/auth.typegen.ts index 3843a6d7cb..71a4e6dc39 100644 --- a/machines/auth.typegen.ts +++ b/machines/auth.typegen.ts @@ -2,8 +2,8 @@ export interface Typegen0 { '@@xstate/typegen': true; - 'internalEvents': { - '': { type: '' }; + internalEvents: { + '': {type: ''}; 'done.invoke.auth.authorized:invocation[0]': { type: 'done.invoke.auth.authorized:invocation[0]'; data: unknown; @@ -14,19 +14,19 @@ export interface Typegen0 { data: unknown; __tip: 'See the XState TS docs to learn how to strongly type this.'; }; - 'xstate.init': { type: 'xstate.init' }; + 'xstate.init': {type: 'xstate.init'}; }; - 'invokeSrcNameMap': { + invokeSrcNameMap: { downloadFaceSdkModel: 'done.invoke.auth.authorized:invocation[0]'; generatePasscodeSalt: 'done.invoke.auth.introSlider:invocation[0]'; }; - 'missingImplementations': { + missingImplementations: { actions: never; delays: never; guards: never; services: never; }; - 'eventsCausingActions': { + eventsCausingActions: { requestStoredContext: 'xstate.init'; setBiometrics: 'SETUP_BIOMETRICS'; setContext: 'STORE_RESPONSE'; @@ -40,18 +40,18 @@ export interface Typegen0 { | 'done.invoke.auth.authorized:invocation[0]' | 'done.invoke.auth.introSlider:invocation[0]'; }; - 'eventsCausingDelays': {}; - 'eventsCausingGuards': { + eventsCausingDelays: {}; + eventsCausingGuards: { hasBiometricSet: ''; hasData: 'STORE_RESPONSE'; hasLanguageset: ''; hasPasscodeSet: ''; }; - 'eventsCausingServices': { + eventsCausingServices: { downloadFaceSdkModel: 'LOGIN' | 'SETUP_PASSCODE'; generatePasscodeSalt: 'SELECT'; }; - 'matchesStates': + matchesStates: | 'authorized' | 'checkingAuth' | 'init' @@ -60,5 +60,5 @@ export interface Typegen0 { | 'savingDefaults' | 'settingUp' | 'unauthorized'; - 'tags': never; + tags: never; } diff --git a/machines/bleShare/request/requestMachine.typegen.ts b/machines/bleShare/request/requestMachine.typegen.ts index 3ed41328a8..4a8b103887 100644 --- a/machines/bleShare/request/requestMachine.typegen.ts +++ b/machines/bleShare/request/requestMachine.typegen.ts @@ -66,11 +66,11 @@ export interface Typegen0 { prependReceivedVc: 'VC_RESPONSE'; registerLoggers: | 'DISCONNECT' - | 'DISMISS' + | 'RESET' | 'xstate.after(DESTROY_TIMEOUT)#request.clearingConnection'; removeLoggers: | 'DISCONNECT' - | 'DISMISS' + | 'RESET' | 'SCREEN_BLUR' | 'xstate.after(DESTROY_TIMEOUT)#request.clearingConnection' | 'xstate.init'; @@ -105,7 +105,7 @@ export interface Typegen0 { eventsCausingServices: { advertiseDevice: | 'DISCONNECT' - | 'DISMISS' + | 'RESET' | 'xstate.after(DESTROY_TIMEOUT)#request.clearingConnection'; checkBluetoothService: 'NEARBY_ENABLED'; checkNearByDevicesPermission: diff --git a/machines/issuersMachine.ts b/machines/issuersMachine.ts index 1ddf71b164..876535a308 100644 --- a/machines/issuersMachine.ts +++ b/machines/issuersMachine.ts @@ -2,9 +2,9 @@ import {authorize, AuthorizeResult} from 'react-native-app-auth'; import {assign, EventFrom, send, sendParent, StateFrom} from 'xstate'; import {createModel} from 'xstate/lib/model'; import {MY_VCS_STORE_KEY} from '../shared/constants'; -import {request} from '../shared/request'; import {StoreEvents} from './store'; import {AppServices} from '../shared/GlobalContext'; +import NetInfo from '@react-native-community/netinfo'; import { generateKeys, isCustomSecureKeystore, @@ -14,18 +14,27 @@ import {KeyPair} from 'react-native-rsa-native'; import {ActivityLogEvents} from './activityLog'; import {log} from 'xstate/lib/actions'; import {verifyCredential} from '../shared/vcjs/verifyCredential'; -import {getBody, getIdentifier} from '../shared/openId4VCI/Utils'; +import { + getBody, + getIdentifier, + vcDownloadTimeout, + OIDCErrors, + ErrorMessage, +} from '../shared/openId4VCI/Utils'; +import {NETWORK_REQUEST_FAILED, REQUEST_TIMEOUT} from '../shared/constants'; import {VCMetadata} from '../shared/VCMetadata'; import { CredentialWrapper, VerifiableCredential, } from '../types/VC/EsignetMosipVC/vc'; import {CACHED_API} from '../shared/api'; +import {request} from '../shared/request'; const model = createModel( { issuers: [] as issuerType[], - selectedIssuer: [] as issuerType[], + selectedIssuerId: '' as string, + selectedIssuer: {} as issuerType, tokenResponse: {} as AuthorizeResult, errorMessage: '' as string, loadingReason: 'displayIssuers' as string, @@ -56,7 +65,7 @@ export const Issuer_Tab_Ref_Id = 'issuersMachine'; export const Issuers_Key_Ref = 'OpenId4VCI'; export const IssuersMachine = model.createMachine( { - /** @xstate-layout N4IgpgJg5mDOIC5QEtawK5gE6wLIEMBjAC2QDswA6AG1QBcBJNTHAYggHsLLY786qqDNjxFS3WrybCcAbQAMAXUSgADh1jI6yLipAAPRAEYA7PMomj8gJwAWeSYAcTgMy2TJgDQgAnsZPWlLYArC5GAEzWEfKRtka2AL4J3kIsoiTkVLBg1GCE2mRQ0iysACIA8gDqAHIAMuUAgqUA+gBqDA3NDKUKykgg6prauv2GCMHuFo7W8sET8i4x1uHefgimJpTWoRHWJrYuc9YAbEkpzCIEGdzZufnkRRdYrADKAKK1bwDCACpvLQwXi8AKpvABKvT0gy0OjIejGE02Thmc3si0iK18iEcRiC1nxJxR22OjhcZxAqUuYkyPByeQKjxkZUBuEBL0h-Whwzho0QiKmKPm6OWq0Q4X2QXkx3C0uCOKM0pcJnJlJwV3EWTp+WK2HYXCyfAElFV6Q1tLujCeHLUGhhI1AY2mx0oRmCxyMjgcJMi8VF6xM4Uo4QWRjc2xMcqVp2SFKepppqmwADMOFgALYNdB0Yip5AAL34sNYDWBPwAEuUwQwAFr-a0DW3c+HGBaOILHeSOWwkj3hYIKv34rYmY4TUeOZzSxzR84yePcRNYFPpzPZ3MF7msMHfN4MVr-Zo-coAaTe1XrXNhzfW4VvlEW3YVMRcexlfo2Wx2kX2h2C20SMYmuqNIwHQXxYJAYBkNo+DUAAYlgHBpjqzycDchqCHGwHcKB4GQdByCwQhSEoRejZXry6zuG2yy9scSq0bY75WEGMpytM+wONsZLkmQHAQHAehAdSFBQuR9oGIgAC0xx+jJKpYSJghkFoYlDBRDqIMctiUB2EyOPpMzhM4mJrH2gTLHs8jxI44T2K6ClzthVCSJac5qXaPKaQgtimWK1lBJExwzF24QuMGtgAbOaTOea9IPChHlNpRLi2ZQ-Zur5o7PlEslYggwa4r5JwBFlLijvsjkxUpcXak8SUaZJCARi4Lq2F28izGEna2e+dgugExwmKl7hmFKVVUtcVCLsuGZZjmWD5oWEmXhJYxWCxtkxGiRjLJYjh+oVgUlXYMrlcElWAYpU2ULhEECQRRGIch9WcuJXlNfECqBR6MTOHYZgHflwZtkNVkxGdrbhBNao1cgEC5A1a2IC1bUdV1VgTn5BV7JQWOksENhyk4RhJEkQA */ + /** @xstate-layout N4IgpgJg5mDOIC5QEtawK5gE6wLIEMBjAC2QDswA6CVABwBt8BPASTUxwGIIB7Cy8gDceAayqoM2PEVL8asBszaScCIT0L4ALsj4BtAAwBdQ0cShaPWMh19zIAB6IALAEYAnJWfuAzM4BsAKwAHN7BwQDsfgA0IEyIAEw+Ea6UBgkRwT7uEekGrhHOAL5FsRIc0iTkVPKKrOxSnNhYPFiUiloAZq0AtgINOARVcnSM9SqwamTCmrZkpqb2ltZz9k4Izgme3n5Boe7hUc6x8QgJ3gZpgT6BgRH+yYWBJWUDlbJUza2cACoASgBNAD6AEEAOIglgAOUWSBAyxsujIaxcWy8vgCITCkRicUSIQSlGyCQMETu7n84QKLxA5SkQw+lC+WF+gNBEOhelcZjhCNWcPWrgSJ0QQppdMGMmqTKwLRZ-2B4MhMISPIsVkRdgFooMvjS-jc9wezl1ARFZwiCUClFcN3czmcKWuPnFbwZ0uZnD+AFEAMren5A71-P4AeT+sPVKyRKIQrgMwX8lASCWCgVcgW8lt15oyEU8CSCloz6WcN38rom7v4sDA9DAhB0ZCgyg4nAAIqGAOpQgAyoZB7aBLHbkfhGv5oHWPgMBmclEigQM-lTVpyc9z+R8yfzriyPhTS4zlYq1aotfrjfILYGnH9ve9AGEft6hyxfb6AKrBsd8mPahBAmFPEEB8VwhS8XVkgKA5dXcE96SlOQeAAdzIegeHwCBW2wR8+E6ZAoG4PhxGmURxDdJCalQ9DMOwgY8LIAioCmGZtCRBZjCWCd-ynUVgNOMVSlpSjhmotCMKwnCsEY5imllVp2kYLpen6KsqOoGjJPolRZMI1iNHY-RjF-HitT4jYUwXZwgMpFMfHCGcIlze15zuFdghyA5UweBDJTEygSAbEQWDILRsAoLRiP4dQxEC4hgtC8KsEi0zo3MxxRVuQlLUCdxdUzbIhX8c14w8Iky1ucI1zTYI-PeaUgsIEKwoisAot4GKyLipqWuS1LuW49LkQA4I0SFdxwOXO1-HSUrbUuLZSX8XwMn8Vwy3qs94sS1qUva+S5SU7RuiwPpeqStqtDSzURos4IIMpB5i1moCHPNSbgi8QIDXcPLcltDIto0i69simU5QSVlFQ5GEuN5My7syuNkiTfJJvyDwiyCUrZ2tPwbimoDMhuYGAtobBTp6EF0C0YhWmQAAvIyyGi0jhDiiUGv4CmsCpmm6YZ5m5gM2YOJM+Go1u2NbJtFbJoyN7U3cc0V2tbxXH8FdHSPe0ycZXn+dp+msCZlnDsUjoqbU08NMN3oBZNs2RfUMXjJMSXx2GmXNbl9wFbJJJlfNB6bUiLYbiW-KIgifXpXts7HaF83mWOlSzptxDycph3jeTl2yLd+YTMGhHvYA64k01-2hUDg9PNK5J52CAwZ3TQpwNj4Sue23qAGkwCYAAFfBkBZR8AAknz7oE++9YEh8hCNPb-DLBTnKuPD+h15cKc0qptDaMcibxnm70TGX7weR7HzhJ+n2f56BReWAjUupcnZH4x8Tf-czAJ-Z7xAskS4YFiQeXOJ5F0591IBRgBQLA2gwAD2HqPFknV2bkUzv5Rk8DsBIJQTfLAosWacTVF7aWAECjZEoPlTy6Z1o5HOOaPwqRqoOQyGNTMKY478DwYg8KhC0FswEN1CisDcFgAQQQ6+aCSFzAWO-Chn9BRRE8HQv6msPCWmOCBFMhI0xBH8DHMkOQyS8PErRLCj4sCQCkTofA9BYAiNiuI22AVeASTojYuxYVkCOMmK7UhEtyGryRusGyER9SWgpIVCkYFAi5j3EmG4CZW4OSbsUGB7jGSeKsRAHxEB7H+KcRbNoVtVI9w0nk7ShTikBPkeLD2oTEaxkidErYQQyzxIzLmThlBKSrQzJsKkFjNJeOsbYopfiAl3xBFCR83pew3RUS4R0HTYndIeL0kCe4ogVSyB4ByMcNpjMENgZAnQmDXjqTM+gLixHYO5lQc5psrk3KmfU+gjT3YrN4l-eMnguF3HOAkDMDlEl6ITFXYxDosjnCONA14EjpSvMudc5styHH3NThUjOVSApoveZiz5dyfnF2aUNShFkppAqtCCzY4K0x9IMNafMDCDgPBJAeMZyAID1jvqGXAQ8HwvlHCvVpVC1ZeG5VsVMUQ9l9JNAM5wY0W6pnjJEXl-KwBzIWUsv5a9RTSs2K3OVOJFV6NtKkG4eVUytx+jymkZAeBFPgHCAlHwqWrIQAAWh-uaX1ZIw6WnuJVKIK0EhjNqGMaS7qP7-PWJaXMP0okmhTPmGOFJ4wVmyVnRkzJvWJsQC3IFOQNqJkKPmG4uZMiXF1JSTYhZ0i7DGReBsTYbwqCLUauMu4iRUh+pmIUG0km2gGQURMqNNibTzTg6UNS6LST0lAHt4TEDGJcoWWhI6xreACGSXNyKcmNQSs1S6+0tBrtjGGgZU6XqsqSMEc0ZJtwFRNAUHeGYz7Hvzae3a-V2oQ1aAka9VCwVROhS3PKfhlwFA+mNZMjxcgORskkI9IkUU8xzonPOpthb-LCW0gSoofB+C8GNFaLd4zLhjmMq+qCx5gYsoehcMcbi7g2imVW5H0yqrGjcPwlo6pzueZQfhMjGNYGY1-PwhISQFGeoJlILCwJeHWhmBVFahK-vnchCZBTSXYvjco4tCAch9LU7Va48ZHSUjORc4lUAsUlJk+sQZyYqrrgNFrZ9VrKR3uMR5TWZGf5tq0AzZsbnSO+0NNkNwbhHKQsEv7bcar8pgW8FsUI2r6zRZRhmNIMdVy5Dyuqyz84AhPXsqqlI0aSL5ZgjawdXSR26NOCmX2WxHS6k-Xxs+JQgA */ predictableActionArguments: true, preserveActionOrder: true, id: Issuer_Tab_Ref_Id, @@ -73,10 +82,11 @@ export const IssuersMachine = model.createMachine( invoke: { src: 'downloadIssuersList', onDone: { - actions: ['setIssuers'], + actions: ['setIssuers', 'resetLoadingReason'], target: 'selectingIssuer', }, onError: { + //loadingReason is not reset here so that we go to previous(Home) screen on back button press of error screen actions: ['setError'], target: 'error', }, @@ -85,13 +95,27 @@ export const IssuersMachine = model.createMachine( error: { description: 'reaches here when any error happens', on: { - TRY_AGAIN: { - actions: 'resetError', - target: 'displayIssuers', - }, + TRY_AGAIN: [ + { + description: 'not fetched issuers config yet', + cond: 'shouldFetchIssuersAgain', + actions: ['setLoadingReasonAsDisplayIssuers', 'resetError'], + target: 'displayIssuers', + }, + { + cond: 'canSelectIssuerAgain', + actions: 'resetError', + target: 'selectingIssuer', + }, + { + description: 'not fetched issuers config yet', + actions: ['setLoadingReasonAsDisplayIssuers', 'resetError'], + target: 'downloadIssuerConfig', + }, + ], RESET_ERROR: { actions: 'resetError', - target: 'idle', + target: 'selectingIssuer', }, }, }, @@ -102,6 +126,7 @@ export const IssuersMachine = model.createMachine( actions: sendParent('DOWNLOAD_ID'), }, SELECTED_ISSUER: { + actions: 'setSelectedIssuerId', target: 'downloadIssuerConfig', }, }, @@ -112,7 +137,32 @@ export const IssuersMachine = model.createMachine( src: 'downloadIssuerConfig', onDone: { actions: 'setSelectedIssuers', - target: 'performAuthorization', + target: 'checkInternet', + }, + onError: { + actions: ['setError', 'resetLoadingReason'], + target: 'error', + }, + }, + }, + checkInternet: { + description: 'checks internet before opening the web view', + invoke: { + src: 'checkInternet', + id: 'checkInternet', + onDone: [ + { + cond: 'isInternetConnected', + target: 'performAuthorization', + }, + { + actions: ['setNoInternet', 'resetLoadingReason'], + target: 'error', + }, + ], + onError: { + actions: () => console.log('checkInternet error caught'), + target: 'error', }, }, }, @@ -122,13 +172,34 @@ export const IssuersMachine = model.createMachine( invoke: { src: 'invokeAuthorization', onDone: { - actions: ['setTokenResponse', 'getKeyPairFromStore', 'loadKeyPair'], + actions: [ + 'setTokenResponse', + 'setLoadingReasonAsSettingUp', + 'getKeyPairFromStore', + 'loadKeyPair', + ], target: 'checkKeyPair', }, - onError: { - actions: [() => console.log('error in invokeAuth - ', event.data)], - target: 'downloadCredentials', - }, + onError: [ + { + cond: 'isOIDCflowCancelled', + actions: ['resetError', 'resetLoadingReason'], + target: 'selectingIssuer', + }, + { + cond: 'isOIDCConfigError', + actions: ['setOIDCConfigError'], + target: 'error', + }, + { + actions: [ + 'setError', + 'resetLoadingReason', + (_, event) => console.log('error in invokeAuth - ', event.data), + ], + target: 'error', + }, + ], }, }, checkKeyPair: { @@ -153,11 +224,20 @@ export const IssuersMachine = model.createMachine( src: 'generateKeyPair', onDone: [ { - actions: ['setPublicKey', 'setPrivateKey', 'storeKeyPair'], + actions: [ + 'setPublicKey', + 'setLoadingReasonAsDownloadingCredentials', + 'setPrivateKey', + 'storeKeyPair', + ], target: 'downloadCredentials', }, { - actions: ['setPublicKey', 'storeKeyPair'], + actions: [ + 'setPublicKey', + 'setLoadingReasonAsDownloadingCredentials', + 'storeKeyPair', + ], cond: 'isCustomSecureKeystore', target: 'downloadCredentials', }, @@ -172,10 +252,12 @@ export const IssuersMachine = model.createMachine( actions: ['setVerifiableCredential', 'setCredentialWrapper'], target: 'verifyingCredential', }, - onError: { - actions: event => - console.log(' error occured in downloadCredential', event), - }, + onError: [ + { + actions: ['setError', 'resetLoadingReason'], + target: 'error', + }, + ], }, on: { CANCEL: { @@ -232,19 +314,39 @@ export const IssuersMachine = model.createMachine( actions: { setIssuers: model.assign({ issuers: (_, event) => event.data, + }), + setNoInternet: model.assign({ + errorMessage: () => ErrorMessage.NO_INTERNET, + }), + setLoadingReasonAsDisplayIssuers: model.assign({ + loadingReason: 'displayIssuers', + }), + setLoadingReasonAsDownloadingCredentials: model.assign({ + loadingReason: 'downloadingCredentials', + }), + setLoadingReasonAsSettingUp: model.assign({ + loadingReason: 'settingUp', + }), + resetLoadingReason: model.assign({ loadingReason: null, }), - setError: model.assign({ errorMessage: (_, event) => { - console.log('Error while fetching issuers ', event.data.message); - return event.data.message === 'Network request failed' - ? 'noInternetConnection' - : 'generic'; + console.log('Error occured ', event.data.message); + const error = event.data.message; + switch (error) { + case NETWORK_REQUEST_FAILED: + return ErrorMessage.NO_INTERNET; + case REQUEST_TIMEOUT: + return ErrorMessage.REQUEST_TIMEDOUT; + default: + return ErrorMessage.GENERIC; + } }, - loadingReason: null, }), - + setOIDCConfigError: model.assign({ + errorMessage: (_, event) => event.data.toString(), + }), resetError: model.assign({ errorMessage: '', }), @@ -268,7 +370,6 @@ export const IssuersMachine = model.createMachine( to: context => context.serviceRefs.store, }, ), - storeVerifiableCredentialMeta: send( context => StoreEvents.PREPEND(MY_VCS_STORE_KEY, getVCMetadata(context)), @@ -316,9 +417,11 @@ export const IssuersMachine = model.createMachine( setSelectedIssuers: model.assign({ selectedIssuer: (_, event) => event.data, }), + setSelectedIssuerId: model.assign({ + selectedIssuerId: (_, event) => event.id, + }), setTokenResponse: model.assign({ tokenResponse: (_, event) => event.data, - loadingReason: 'settingUp', }), setVerifiableCredential: model.assign({ verifiableCredential: (_, event) => { @@ -331,17 +434,16 @@ export const IssuersMachine = model.createMachine( }, }), setPublicKey: assign({ - publicKey: (context, event) => { + publicKey: (_, event) => { if (!isCustomSecureKeystore()) { return (event.data as KeyPair).public; } return event.data as string; }, - loadingReason: 'downloadingCredentials', }), setPrivateKey: assign({ - privateKey: (context, event) => (event.data as KeyPair).private, + privateKey: (_, event) => (event.data as KeyPair).private, }), logDownloaded: send( @@ -363,31 +465,30 @@ export const IssuersMachine = model.createMachine( downloadIssuersList: async () => { return await CACHED_API.fetchIssuers(); }, - - downloadIssuerConfig: async (_, event) => { - return await CACHED_API.fetchIssuerConfig(event.id); + checkInternet: async () => await NetInfo.fetch(), + downloadIssuerConfig: async (context, _) => { + return await CACHED_API.fetchIssuerConfig(context.selectedIssuerId); }, - downloadCredential: async context => { const body = await getBody(context); - const response = await fetch( + const downloadTimeout = await vcDownloadTimeout(); + const response = await request( + 'POST', context.selectedIssuer.serviceConfiguration.credentialEndpoint, + body, + '', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: 'Bearer ' + context.tokenResponse?.accessToken, - }, - body: JSON.stringify(body), + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + context.tokenResponse?.accessToken, }, + downloadTimeout, ); let credential = await response.json(); credential = updateCredentialInformation(context, credential); return credential; }, invokeAuthorization: async context => { - const response = await authorize(context.selectedIssuer); - return response; + return await authorize(context.selectedIssuer); }, generateKeyPair: async context => { if (!isCustomSecureKeystore()) { @@ -408,6 +509,33 @@ export const IssuersMachine = model.createMachine( hasKeyPair: context => { return context.publicKey != null; }, + isInternetConnected: (_, event) => !!event.data.isConnected, + isOIDCflowCancelled: (_, event) => { + // iOS & Android have different error strings for user cancelled flow + const err = [ + OIDCErrors.OIDC_FLOW_CANCELLED_ANDROID, + OIDCErrors.OIDC_FLOW_CANCELLED_IOS, + ]; + return ( + !!event.data && + typeof event.data.toString === 'function' && + err.some(e => event.data.toString().includes(e)) + ); + }, + isOIDCConfigError: (_, event) => { + return ( + !!event.data && + typeof event.data.toString === 'function' && + event.data.toString().includes(OIDCErrors.OIDC_CONFIG_ERROR_PREFIX) + ); + }, + canSelectIssuerAgain: (context, _) => { + return ( + context.errorMessage.includes(OIDCErrors.OIDC_CONFIG_ERROR_PREFIX) || + context.errorMessage.includes(ErrorMessage.REQUEST_TIMEDOUT) + ); + }, + shouldFetchIssuersAgain: context => context.issuers.length === 0, isCustomSecureKeystore: () => isCustomSecureKeystore(), }, }, @@ -419,8 +547,11 @@ export function selectIssuers(state: State) { return state.context.issuers; } -export function selectErrorMessage(state: State) { - return state.context.errorMessage; +export function selectErrorMessageType(state: State) { + return state.context.errorMessage === '' || + state.context.errorMessage === ErrorMessage.NO_INTERNET + ? state.context.errorMessage + : ErrorMessage.GENERIC; } export function selectLoadingReason(state: State) { diff --git a/machines/issuersMachine.typegen.ts b/machines/issuersMachine.typegen.ts index b07dc261d3..9d9bb364e6 100644 --- a/machines/issuersMachine.typegen.ts +++ b/machines/issuersMachine.typegen.ts @@ -3,6 +3,11 @@ export interface Typegen0 { '@@xstate/typegen': true; internalEvents: { + 'done.invoke.checkInternet': { + type: 'done.invoke.checkInternet'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; 'done.invoke.issuersMachine.displayIssuers:invocation[0]': { type: 'done.invoke.issuersMachine.displayIssuers:invocation[0]'; data: unknown; @@ -33,10 +38,22 @@ export interface Typegen0 { data: unknown; __tip: 'See the XState TS docs to learn how to strongly type this.'; }; + 'error.platform.checkInternet': { + type: 'error.platform.checkInternet'; + data: unknown; + }; 'error.platform.issuersMachine.displayIssuers:invocation[0]': { type: 'error.platform.issuersMachine.displayIssuers:invocation[0]'; data: unknown; }; + 'error.platform.issuersMachine.downloadCredentials:invocation[0]': { + type: 'error.platform.issuersMachine.downloadCredentials:invocation[0]'; + data: unknown; + }; + 'error.platform.issuersMachine.downloadIssuerConfig:invocation[0]': { + type: 'error.platform.issuersMachine.downloadIssuerConfig:invocation[0]'; + data: unknown; + }; 'error.platform.issuersMachine.performAuthorization:invocation[0]': { type: 'error.platform.issuersMachine.performAuthorization:invocation[0]'; data: unknown; @@ -44,6 +61,7 @@ export interface Typegen0 { 'xstate.init': {type: 'xstate.init'}; }; invokeSrcNameMap: { + checkInternet: 'done.invoke.checkInternet'; downloadCredential: 'done.invoke.issuersMachine.downloadCredentials:invocation[0]'; downloadIssuerConfig: 'done.invoke.issuersMachine.downloadIssuerConfig:invocation[0]'; downloadIssuersList: 'done.invoke.issuersMachine.displayIssuers:invocation[0]'; @@ -61,12 +79,31 @@ export interface Typegen0 { getKeyPairFromStore: 'done.invoke.issuersMachine.performAuthorization:invocation[0]'; loadKeyPair: 'done.invoke.issuersMachine.performAuthorization:invocation[0]'; logDownloaded: 'done.invoke.issuersMachine.verifyingCredential:invocation[0]'; - resetError: 'RESET_ERROR' | 'TRY_AGAIN'; + resetError: + | 'RESET_ERROR' + | 'TRY_AGAIN' + | 'error.platform.issuersMachine.performAuthorization:invocation[0]'; + resetLoadingReason: + | 'done.invoke.checkInternet' + | 'done.invoke.issuersMachine.displayIssuers:invocation[0]' + | 'error.platform.issuersMachine.downloadCredentials:invocation[0]' + | 'error.platform.issuersMachine.downloadIssuerConfig:invocation[0]' + | 'error.platform.issuersMachine.performAuthorization:invocation[0]'; setCredentialWrapper: 'done.invoke.issuersMachine.downloadCredentials:invocation[0]'; - setError: 'error.platform.issuersMachine.displayIssuers:invocation[0]'; + setError: + | 'error.platform.issuersMachine.displayIssuers:invocation[0]' + | 'error.platform.issuersMachine.downloadCredentials:invocation[0]' + | 'error.platform.issuersMachine.downloadIssuerConfig:invocation[0]' + | 'error.platform.issuersMachine.performAuthorization:invocation[0]'; setIssuers: 'done.invoke.issuersMachine.displayIssuers:invocation[0]'; + setLoadingReasonAsDisplayIssuers: 'TRY_AGAIN'; + setLoadingReasonAsDownloadingCredentials: 'done.invoke.issuersMachine.generateKeyPair:invocation[0]'; + setLoadingReasonAsSettingUp: 'done.invoke.issuersMachine.performAuthorization:invocation[0]'; + setNoInternet: 'done.invoke.checkInternet'; + setOIDCConfigError: 'error.platform.issuersMachine.performAuthorization:invocation[0]'; setPrivateKey: 'done.invoke.issuersMachine.generateKeyPair:invocation[0]'; setPublicKey: 'done.invoke.issuersMachine.generateKeyPair:invocation[0]'; + setSelectedIssuerId: 'SELECTED_ISSUER'; setSelectedIssuers: 'done.invoke.issuersMachine.downloadIssuerConfig:invocation[0]'; setTokenResponse: 'done.invoke.issuersMachine.performAuthorization:invocation[0]'; setVerifiableCredential: 'done.invoke.issuersMachine.downloadCredentials:invocation[0]'; @@ -78,21 +115,27 @@ export interface Typegen0 { }; eventsCausingDelays: {}; eventsCausingGuards: { + canSelectIssuerAgain: 'TRY_AGAIN'; hasKeyPair: 'CHECK_KEY_PAIR'; isCustomSecureKeystore: 'done.invoke.issuersMachine.generateKeyPair:invocation[0]'; + isInternetConnected: 'done.invoke.checkInternet'; + isOIDCConfigError: 'error.platform.issuersMachine.performAuthorization:invocation[0]'; + isOIDCflowCancelled: 'error.platform.issuersMachine.performAuthorization:invocation[0]'; + shouldFetchIssuersAgain: 'TRY_AGAIN'; }; eventsCausingServices: { + checkInternet: 'done.invoke.issuersMachine.downloadIssuerConfig:invocation[0]'; downloadCredential: | 'CHECK_KEY_PAIR' - | 'done.invoke.issuersMachine.generateKeyPair:invocation[0]' - | 'error.platform.issuersMachine.performAuthorization:invocation[0]'; - downloadIssuerConfig: 'SELECTED_ISSUER'; + | 'done.invoke.issuersMachine.generateKeyPair:invocation[0]'; + downloadIssuerConfig: 'SELECTED_ISSUER' | 'TRY_AGAIN'; downloadIssuersList: 'TRY_AGAIN' | 'xstate.init'; generateKeyPair: 'CHECK_KEY_PAIR'; - invokeAuthorization: 'done.invoke.issuersMachine.downloadIssuerConfig:invocation[0]'; + invokeAuthorization: 'done.invoke.checkInternet'; verifyCredential: 'done.invoke.issuersMachine.downloadCredentials:invocation[0]'; }; matchesStates: + | 'checkInternet' | 'checkKeyPair' | 'displayIssuers' | 'done' diff --git a/machines/settings.typegen.ts b/machines/settings.typegen.ts index e616d17f17..31ee28ed65 100644 --- a/machines/settings.typegen.ts +++ b/machines/settings.typegen.ts @@ -32,12 +32,13 @@ export interface Typegen0 { | 'done.invoke.settings.resetInjiProps:invocation[0]' | 'error.platform.settings.resetInjiProps:invocation[0]'; requestStoredContext: 'xstate.init'; - resetCredentialRegistry: 'CANCEL' | 'UPDATE_CREDENTIAL_REGISTRY'; + resetCredentialRegistry: 'CANCEL' | 'UPDATE_MIMOTO_HOST'; setContext: 'STORE_RESPONSE'; storeContext: | 'ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS' | 'STORE_RESPONSE' | 'TOGGLE_BIOMETRIC_UNLOCK' + | 'UPDATE_ESIGNET_HOST' | 'UPDATE_NAME' | 'UPDATE_VC_LABEL' | 'done.invoke.settings.resetInjiProps:invocation[0]'; @@ -46,6 +47,7 @@ export interface Typegen0 { updateCredentialRegistryResponse: 'error.platform.settings.resetInjiProps:invocation[0]'; updateCredentialRegistrySuccess: 'done.invoke.settings.resetInjiProps:invocation[0]'; updateDefaults: 'STORE_RESPONSE'; + updateEsignetHostUrl: 'UPDATE_ESIGNET_HOST'; updateName: 'UPDATE_NAME'; updatePartialDefaults: 'STORE_RESPONSE'; updateUserShownWithHardwareKeystoreNotExists: 'ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS'; @@ -57,7 +59,7 @@ export interface Typegen0 { hasPartialData: 'STORE_RESPONSE'; }; eventsCausingServices: { - resetInjiProps: 'UPDATE_CREDENTIAL_REGISTRY'; + resetInjiProps: 'UPDATE_MIMOTO_HOST'; }; matchesStates: | 'idle' diff --git a/machines/store.typegen.ts b/machines/store.typegen.ts index c689d514de..804f958af5 100644 --- a/machines/store.typegen.ts +++ b/machines/store.typegen.ts @@ -1,8 +1,57 @@ -// This file was automatically generated. Edits will be overwritten +<<<<<<< HEAD + // This file was automatically generated. Edits will be overwritten + + export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + "done.invoke._store": { type: "done.invoke._store"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; +"done.invoke.store.resettingStorage:invocation[0]": { type: "done.invoke.store.resettingStorage:invocation[0]"; data: unknown; __tip: "See the XState TS docs to learn how to strongly type this." }; +"error.platform._store": { type: "error.platform._store"; data: unknown }; +"xstate.init": { type: "xstate.init" }; + }; + invokeSrcNameMap: { + "checkStorageInitialisedOrNot": "done.invoke.store.checkStorageInitialisation:invocation[0]"; +"clear": "done.invoke.store.resettingStorage:invocation[0]"; +"generateEncryptionKey": "done.invoke.store.generatingEncryptionKey:invocation[0]"; +"getEncryptionKey": "done.invoke.store.gettingEncryptionKey:invocation[0]"; +"hasAndroidEncryptionKey": "done.invoke.store.checkEncryptionKey:invocation[0]"; +"store": "done.invoke._store"; + }; + missingImplementations: { + actions: "logKey"; + delays: never; + guards: never; + services: never; + }; + eventsCausingActions: { + "forwardStoreRequest": "APPEND" | "CLEAR" | "GET" | "PREPEND" | "REMOVE" | "REMOVE_ITEMS" | "REMOVE_VC_METADATA" | "SET" | "UPDATE"; +"logKey": "KEY_RECEIVED"; +"notifyParent": "KEY_RECEIVED" | "READY" | "done.invoke.store.resettingStorage:invocation[0]"; +"setEncryptionKey": "KEY_RECEIVED"; + }; + eventsCausingDelays: { + + }; + eventsCausingGuards: { + "isCustomSecureKeystore": "KEY_RECEIVED"; + }; + eventsCausingServices: { + "checkStorageInitialisedOrNot": "ERROR"; +"clear": "KEY_RECEIVED"; +"generateEncryptionKey": "ERROR" | "IGNORE" | "READY"; +"getEncryptionKey": "TRY_AGAIN"; +"hasAndroidEncryptionKey": never; +"store": "KEY_RECEIVED" | "READY" | "done.invoke.store.resettingStorage:invocation[0]"; + }; + matchesStates: "checkEncryptionKey" | "checkStorageInitialisation" | "failedReadingKey" | "generatingEncryptionKey" | "gettingEncryptionKey" | "ready" | "resettingStorage"; + tags: never; + } + +======= export interface Typegen0 { '@@xstate/typegen': true; - 'internalEvents': { + internalEvents: { 'done.invoke._store': { type: 'done.invoke._store'; data: unknown; @@ -13,10 +62,10 @@ export interface Typegen0 { data: unknown; __tip: 'See the XState TS docs to learn how to strongly type this.'; }; - 'error.platform._store': { type: 'error.platform._store'; data: unknown }; - 'xstate.init': { type: 'xstate.init' }; + 'error.platform._store': {type: 'error.platform._store'; data: unknown}; + 'xstate.init': {type: 'xstate.init'}; }; - 'invokeSrcNameMap': { + invokeSrcNameMap: { checkStorageInitialisedOrNot: 'done.invoke.store.checkStorageInitialisation:invocation[0]'; clear: 'done.invoke.store.resettingStorage:invocation[0]'; generateEncryptionKey: 'done.invoke.store.generatingEncryptionKey:invocation[0]'; @@ -24,13 +73,13 @@ export interface Typegen0 { hasAndroidEncryptionKey: 'done.invoke.store.checkEncryptionKey:invocation[0]'; store: 'done.invoke._store'; }; - 'missingImplementations': { + missingImplementations: { actions: 'logKey'; delays: never; guards: never; services: never; }; - 'eventsCausingActions': { + eventsCausingActions: { forwardStoreRequest: | 'APPEND' | 'CLEAR' @@ -50,11 +99,11 @@ export interface Typegen0 { setEncryptionKey: 'KEY_RECEIVED'; setIsTamperedVc: 'TAMPERED_VC'; }; - 'eventsCausingDelays': {}; - 'eventsCausingGuards': { + eventsCausingDelays: {}; + eventsCausingGuards: { isCustomSecureKeystore: 'KEY_RECEIVED'; }; - 'eventsCausingServices': { + eventsCausingServices: { checkStorageInitialisedOrNot: 'ERROR'; clear: 'KEY_RECEIVED'; generateEncryptionKey: 'ERROR' | 'IGNORE' | 'READY'; @@ -65,7 +114,7 @@ export interface Typegen0 { | 'READY' | 'done.invoke.store.resettingStorage:invocation[0]'; }; - 'matchesStates': + matchesStates: | 'checkEncryptionKey' | 'checkStorageInitialisation' | 'failedReadingKey' @@ -73,5 +122,6 @@ export interface Typegen0 { | 'gettingEncryptionKey' | 'ready' | 'resettingStorage'; - 'tags': never; + tags: never; } +>>>>>>> 5b53b069 (refactor(INJI-205): Check Internet before OIDC AuthZ) diff --git a/screens/Issuers/IssuerScreenController.tsx b/screens/Issuers/IssuerScreenController.tsx index 63f10d4ec9..a22129f4af 100644 --- a/screens/Issuers/IssuerScreenController.tsx +++ b/screens/Issuers/IssuerScreenController.tsx @@ -2,7 +2,7 @@ import {useSelector} from '@xstate/react'; import { IssuerScreenTabEvents, IssuersMachine, - selectErrorMessage, + selectErrorMessageType, selectIsDone, selectIsDownloadCredentials, selectIsIdle, @@ -18,7 +18,7 @@ export function useIssuerScreenController({route, navigation}) { return { issuers: useSelector(service, selectIssuers), - errorMessage: useSelector(service, selectErrorMessage), + errorMessageType: useSelector(service, selectErrorMessageType), isDownloadingCredentials: useSelector(service, selectIsDownloadCredentials), isDone: useSelector(service, selectIsDone), isIdle: useSelector(service, selectIsIdle), diff --git a/screens/Issuers/IssuersScreen.tsx b/screens/Issuers/IssuersScreen.tsx index 3e969cb250..398cb31998 100644 --- a/screens/Issuers/IssuersScreen.tsx +++ b/screens/Issuers/IssuersScreen.tsx @@ -11,6 +11,7 @@ import {HomeRouteProps} from '../../routes/main'; import {useIssuerScreenController} from './IssuerScreenController'; import {Loader} from '../../components/ui/Loader'; import testIDProps, {removeWhiteSpace} from '../../shared/commonUtil'; +import {ErrorMessage} from '../../shared/openId4VCI/Utils'; export const IssuersScreen: React.FC< HomeRouteProps | RootRouteProps @@ -19,7 +20,7 @@ export const IssuersScreen: React.FC< const {t} = useTranslation('IssuersScreen'); useLayoutEffect(() => { - if (controller.loadingReason || controller.errorMessage) { + if (controller.loadingReason || controller.errorMessageType) { props.navigation.setOptions({ headerShown: false, }); @@ -39,7 +40,11 @@ export const IssuersScreen: React.FC< if (controller.isStoring) { props.navigation.goBack(); } - }, [controller.loadingReason, controller.errorMessage, controller.isStoring]); + }, [ + controller.loadingReason, + controller.errorMessageType, + controller.isStoring, + ]); const onPressHandler = (id: string) => { if (id !== 'UIN, VID, AID') { @@ -50,14 +55,18 @@ export const IssuersScreen: React.FC< }; const isGenericError = () => { - return controller.errorMessage === 'generic'; + return controller.errorMessageType === ErrorMessage.GENERIC; }; const goBack = () => { - controller.RESET_ERROR(); - setTimeout(() => { + if ( + controller.errorMessageType && + controller.loadingReason === 'displayIssuers' + ) { props.navigation.goBack(); - }, 0); + } else { + controller.RESET_ERROR(); + } }; const getImage = () => { @@ -78,6 +87,20 @@ export const IssuersScreen: React.FC< ); }; + if (controller.errorMessageType) { + return ( + + ); + } + if (controller.loadingReason) { return ( )} - {controller.errorMessage && ( - - )} ); }; diff --git a/shared/GlobalContext.ts b/shared/GlobalContext.ts index d93dca3afc..43be0f4aaa 100644 --- a/shared/GlobalContext.ts +++ b/shared/GlobalContext.ts @@ -1,14 +1,14 @@ -import { createContext } from 'react'; -import { ActorRefFrom, InterpreterFrom } from 'xstate'; -import { activityLogMachine } from '../machines/activityLog'; -import { appMachine } from '../machines/app'; -import { authMachine } from '../machines/auth'; -import { requestMachine } from '../machines/bleShare/request/requestMachine'; -import { scanMachine } from '../machines/bleShare/scan/scanMachine'; -import { settingsMachine } from '../machines/settings'; -import { storeMachine } from '../machines/store'; -import { vcMachine } from '../machines/vc'; -import { revokeVidsMachine } from '../machines/revoke'; +import {createContext} from 'react'; +import {ActorRefFrom, InterpreterFrom} from 'xstate'; +import {activityLogMachine} from '../machines/activityLog'; +import {appMachine} from '../machines/app'; +import {authMachine} from '../machines/auth'; +import {requestMachine} from '../machines/bleShare/request/requestMachine'; +import {scanMachine} from '../machines/bleShare/scan/scanMachine'; +import {settingsMachine} from '../machines/settings'; +import {storeMachine} from '../machines/store'; +import {vcMachine} from '../machines/vc'; +import {revokeVidsMachine} from '../machines/revoke'; export const GlobalContext = createContext({} as GlobalServices); diff --git a/shared/InitialConfig.ts b/shared/InitialConfig.ts index 44b37075d9..3f0b65c4d8 100644 --- a/shared/InitialConfig.ts +++ b/shared/InitialConfig.ts @@ -17,5 +17,6 @@ export const INITIAL_CONFIG = { warningDomainName: '', aboutInjiUrl: 'https://docs.mosip.io/inji', faceSdkModelUrl: '', + openId4VCIDownloadVCTimeout: '30000', }, }; diff --git a/shared/constants.ts b/shared/constants.ts index c194a6c77f..cce9377d04 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -68,3 +68,6 @@ export const argon2iConfigForUinVid: Argon2iConfig = { export const argon2iSalt = '1234567891011121314151617181920212223242526272829303132333435363'; + +export const NETWORK_REQUEST_FAILED = 'Network request failed'; +export const REQUEST_TIMEOUT = 'request timedout'; diff --git a/shared/openId4VCI/Utils.ts b/shared/openId4VCI/Utils.ts index 5d9e290e96..30c46017bb 100644 --- a/shared/openId4VCI/Utils.ts +++ b/shared/openId4VCI/Utils.ts @@ -5,6 +5,7 @@ import {isIOS} from '../constants'; import pem2jwk from 'simple-pem2jwk'; import {Issuers_Key_Ref} from '../../machines/issuersMachine'; import {ENABLE_OPENID_FOR_VC} from 'react-native-dotenv'; +import getAllConfigurations from '../commonprops/commonProps'; export const OpenId4VCIProtocol = 'OpenId4VCIProtocol'; export const isOpenId4VCIEnabled = () => { @@ -92,3 +93,24 @@ export const getJWT = async context => { throw e; } }; + +export const vcDownloadTimeout = async (): Promise => { + const response = await getAllConfigurations(); + + return Number(response['openId4VCIDownloadVCTimeout']) || 30000; +}; + +// OIDCErrors is a collection of external errors from the OpenID library or the issuer +export enum OIDCErrors { + OIDC_FLOW_CANCELLED_ANDROID = 'User cancelled flow', + OIDC_FLOW_CANCELLED_IOS = 'org.openid.appauth.general error -3', + + INVALID_TOKEN_SPECIFIED = 'Invalid token specified', + OIDC_CONFIG_ERROR_PREFIX = 'Config error', +} +// ErrorMessage is the type of error message shown in the UI +export enum ErrorMessage { + NO_INTERNET = 'noInternetConnection', + GENERIC = 'generic', + REQUEST_TIMEDOUT = 'requestTimedOut', +} diff --git a/shared/request.ts b/shared/request.ts index 16a0837c30..5ea47bfd66 100644 --- a/shared/request.ts +++ b/shared/request.ts @@ -3,7 +3,7 @@ import { VerifiableCredential, } from '../types/VC/ExistingMosipVC/vc'; import {__AppId} from './GlobalVariables'; -import {HOST, MIMOTO_BASE_URL} from './constants'; +import {MIMOTO_BASE_URL, REQUEST_TIMEOUT} from './constants'; export type HTTP_METHOD = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; @@ -19,18 +19,43 @@ export async function request( path: `/${string}`, body?: Record, host = MIMOTO_BASE_URL, -) { - const headers = { + headers: Record = { 'Content-Type': 'application/json', - }; + }, + timeoutMillis?: undefined | number, +) { if (path.includes('residentmobileapp')) headers['X-AppId'] = __AppId.getValue(); - - const response = await fetch(host + path, { - method, - headers, - body: JSON.stringify(body), - }); + let response; + if (timeoutMillis === undefined) { + response = await fetch(host + path, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + } else { + console.log(`making a web request to ${host + path}`); + let controller = new AbortController(); + setTimeout(() => { + controller.abort(); + }, timeoutMillis); + try { + response = await fetch(host + path, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + signal: controller.signal, + }); + } catch (error) { + console.log( + `Error occurred while making request: ${host + path}: ${error}`, + ); + if (error.name === 'AbortError') { + throw new Error(REQUEST_TIMEOUT); + } + throw error; + } + } const jsonResponse = await response.json(); @@ -38,10 +63,7 @@ export async function request( let backendUrl = host + path; let errorMessage = jsonResponse.message || jsonResponse.error; console.error( - 'The backend API ' + - backendUrl + - ' returned error code 400 with message --> ' + - errorMessage, + `The backend API ${backendUrl} returned error code ${response.status} with message --> ${errorMessage}`, ); throw new Error(errorMessage); }