diff --git a/.talismanrc b/.talismanrc index 00f6afec..b6e737b1 100644 --- a/.talismanrc +++ b/.talismanrc @@ -2,7 +2,7 @@ fileignoreconfig: - filename: package.json checksum: 5b4fcb5ddc7cc96cc2d1733b544d56ea66e88cdab995a1052fbf9ac0e9c2dc21 - filename: package-lock.json - checksum: 98f4ef19f06521bac3ea3033d82810203214bf55b0469790a1d8acc20933c581 + checksum: 84234f4ae94673b929b5355259c4fa0e497d4ec327985f497120b6fb66bb8c63 - filename: lib/jsonld-signatures/suites/ed255192018/ed25519.ts checksum: 493b6e31144116cb612c24d98b97d8adcad5609c0a52c865a6847ced0a0ddc3a - filename: components/PasscodeVerify.tsx @@ -38,9 +38,9 @@ fileignoreconfig: - filename: screens/Home/MyVcs/GetIdInputModal.tsx checksum: 5c736ed79a372d0ffa7c02eb33d0dc06edbbb08d120978ff287f5f06cd6c7746 - filename: shared/openId4VCI/Utils.ts - checksum: ee4db1768be8d51fac0eb876a7b16fd2ab1806abcc711f01056f672003d11f31 + checksum: 78473ce25cd52c8d07da9ba98683bdb64ae83a9de8fa907c62f2ebeca7dd21dc - filename: shared/cryptoutil/cryptoUtil.ts - checksum: a8edd1047e33bfc9e37b73945b8edcd294b8e29baf380f86cb0f647b355c8e5a + checksum: 281f061ae6eb722c75c2caf2bdfb5b1bf72f292b471318d972c898c5a972c65f - filename: shared/telemetry/TelemetryConstants.js checksum: fd8dc3a69cdef68855dc5f0531d8e634bfa2621bb4dc22f85b8247512a349c4c - filename: shared/telemetry/TelemetryUtils.js @@ -66,11 +66,11 @@ fileignoreconfig: - filename: screens/MainLayout.tsx checksum: 53ead79279c9609e42a8993db1a66bdb4649e1ae3b909d462b45b00c507c416e - filename: android/app/build.gradle - checksum: 46a4054440361b25d13ecd75811bf239a6abb4830ce7f79b2b15ccd878758760 + checksum: 251ac12b49cf226d40931a02eb1b362a1d29e74cfe8f7a60ed128d66b0fea838 - filename: .github/workflows/push-triggers.yml checksum: abc19ea38c8d7b79f15695d015709cc88a34a995181aaf12bc8344f940f3cbc4 - filename: android/fastlane/Fastfile - checksum: a25f155bcbbae7ab09563637c23771f7349738f12a6ddc8ae71c29c61ed535af + checksum: a5e816489a80b0a7498f35b7a2919f2287d4307b660687c2a9c51412aa8eceb7 - filename: .github/workflows/internal-build.yml checksum: e9b85cf0405d777faee9345269f6f9eb861ed205728dca63cf27a5db79c876a7 - filename: assets/Issuer_search_clearing_button.svg @@ -82,7 +82,7 @@ fileignoreconfig: - filename: screens/Settings/BackupViaPassword.tsx checksum: d2a355356bcaf8f7ef3b53ba93710cec15fefd0fdf31efd779eebd2bfab61c19 - filename: shared/api.ts - checksum: 1c5d43058e8733a403e02d0b3fa5f56e11519efa4564e48c92b4491f4bd60508 + checksum: 05165e469008816a441126f8eed69d54c137d39c3c66e695f8710e8d33a9b038 - filename: machines/backupWithEncryption.ts checksum: 038c12d30b2312fcbd9230a1c6ddb494d2e561fe0d09741335fa80ab67e2c550 - filename: machines/backupWithEncryption.typegen.ts @@ -154,9 +154,9 @@ fileignoreconfig: - filename: injitest/README.md checksum: 82974a6b9363512472272245e9b433f92e63377e58ba306980876b745181a09c - filename: shared/VCMetadata.ts - checksum: e93f988415bf91064e2cf5fbc09ff6c7226798baa5da721fa0715d5d0d6afddf + checksum: 4c0f2acc58894e5a427e1317b38d04daff91f64d1e61d6ee2f246ee516ef97ca - filename: ios/Podfile.lock - checksum: b8c97d58a88207bae811db83074388cff249a83055a1f92ea7dee2f59b7a32c9 + checksum: 43bd4742f2ba13357d8b9c44430bfa3cca0bf9bf8341984fd81174a929c85955 - filename: components/BackupAndRestoreBannerNotification.tsx checksum: e465a9947727687d784d0cb9d8db1e28f765b0659bf4a3aa6d75643aa7b14102 - filename: components/ActivityLogEvent.ts @@ -205,6 +205,10 @@ fileignoreconfig: checksum: 4b08ee05c9c6fe9154ed3cdc9d23324e4d9cfac8a7028686f2b22903424d4cef - filename: machines/VCItemMachine/VCItemGaurds.ts checksum: 4f32814fc26a0edaa54a42dbc9f9e1d899144eb059ac8da211d1738887871829 + - filename: machines/VerifiableCredential/VCItemMachine/VCItemGaurds.ts + checksum: 797069975e6402527d506db8f4644586957b8437d9cf867eaed4b0000683c4f8 + - filename: ios/RNSecureKeystoreModule.m + checksum: d3f70ec17eacdfc3ed64936be1fc14b83fd1004c9f861fb0827da8df28ac3bf9 - filename: machines/VCItemMachine/VCItemServices.ts checksum: 51b4872a64abd76b124000358068c0b213d50fb131d735c122cd9a177cd8390c - filename: machines/VCItemMachine/VCItemActions.ts @@ -238,7 +242,7 @@ fileignoreconfig: - filename: machines/backupAndRestore/backupRestore.typegen.ts checksum: 64a8e42712083e0cbf0d6ce6b1139c62b59a14af17e4132d14f903d4d3bbe6b2 - filename: machines/app.typegen.ts - checksum: c310de13b855b22fe68e8fa954dc3a8a4728284c5904da99986a1f4eac8ea0be + checksum: 6e194b5eb5f2ae91627bb1db832e0c9ab959f376593498df839db964fecf4258 - filename: screens/Home/MyVcsTabMachine.typegen.ts checksum: 948efb4d61551e4f3cd9eb9913b927158daa5c3d16f49ad297e7cb63190bc023 - filename: machines/IssuersMachine.typegen.ts @@ -246,7 +250,7 @@ fileignoreconfig: - filename: screens/Home/MyVcsTab.tsx checksum: ccf9ca41165b35628e026e7557f6fa7771f29dae8b09324d9af93c2c9b0eca6f - filename: machines/store.typegen.ts - checksum: 46f3a7c2d15ed03fc70e27ecae5a12c128011c49913b35cdb8edba12b1a999db + checksum: 077a1906c908daf79544f604632df36f3359075a7bc912e51ab5707cb178bf48 - filename: machines/VerifiableCredential/VCMetaMachine/VCMetaMachine.ts checksum: 36244323200d1e965adb48205d359fba807a5a153f2fc3ce75fe34e8b1bdb01a - filename: machines/Issuers/IssuersMachine.ts @@ -256,18 +260,17 @@ fileignoreconfig: - filename: screens/Issuers/CredentialTypeSelectionScreen.tsx checksum: 144bbf59e86a89bf580ac7931645ca3eaed69a9409de36f6ce9f88a14091a9d3 - filename: components/QrCodeOverlay.tsx - checksum: 68758cbddafe575c7f53294327963112a0780d30fd23cd0b6bf82d7dcfd856fe - checksum: 47220a4ebd8af702afe622a29b689f651eb23387bac1c623f241839beb413d25 + checksum: dacbce1d6cd7e702400897981f44b65288541f4a41ee970f1e6ac1146af150bb - filename: machines/Issuers/IssuersEvents.ts checksum: fd8c30e0cf43a784be883c9d79a3bff0d2bcd9075e937d225939040542998b10 - filename: machines/Issuers/IssuersGuards.ts - checksum: d87b6f4277c4be68f1884efa5c73e1b1d02a1afaefb276417b95a312f599578a + checksum: 21783a057207ad04facdb4c71884f49b0230490def04158419d730e0cc60eb83 - filename: machines/Issuers/IssuersActions.ts - checksum: e865a33dcecfe185eff5ac06208f0f2e8ff6574f778e31da74d6ba74c67e285d + checksum: 4414aa10588d2305293b1902982c5969895c858355e4b91d01dfaa8601c2dd62 - filename: injitest/automation_trigger.sh checksum: f2f34839c99cb1b871dde17aed8508a071345d22738796e005ff709d2dab8644 - filename: machines/Issuers/IssuersService.ts - checksum: 1b54b0249488fc496e3582588c644034865fd50386757789baaaf1c3821464d5 + checksum: e3832dff27687abc28609d2b281e570b4b0017995b7cfb56627a6b96949c469a - filename: screens/Home/ViewVcModal.tsx checksum: cfb25d562185488432b76287c4ef93359c1c64d8e29f5755d4c0a726c1485442 - filename: injitest/src/main/resources/TestData.json @@ -293,7 +296,7 @@ fileignoreconfig: - filename: machines/QrLogin/QrLoginMachine.ts checksum: f4234549baea9a7f69be98f52c30a04f2f6138f9b1f2b60a7b40f15f7e03345b - filename: machines/QrLogin/QrLoginServices.ts - checksum: b20d0caa6d23078b4010ea5185f01270356422dd216edd7834b069cdedd3383d + checksum: 0eee865344c9e15722bac2307b5dd6025fd809233b71456472609d578c9a365a - filename: machines/bleShare/scan/scanActions.ts checksum: e32e1be4a02f9247844771293e5fc285bd6e1bfd3f0451a6bbc5bd171931b6f2 - filename: injitest/src/test/java/iosTestCases/ActivateVcTest.java @@ -303,7 +306,7 @@ fileignoreconfig: - filename: injitest/src/test/java/iosTestCases/VerifyHistoryTest.java checksum: 8a00278af4c8744c713c57328991bbca438eb5d5d89b492a7f5234c47362f44b - filename: machines/store.ts - checksum: 93ffa32067d698ecc9b7c1eec3b58ce1b3bee44d53c0e7daded0467393e3f0d6 + checksum: 3fd2db0c41f8bd5f30ef922b856549cb5423997b2123c5364e643e47e5efd3cf - filename: components/BannerNotificationContainer.tsx checksum: 9e5b4a61b87e86666f0bee550d410df2b8576dfe5ec374de0ab139a468a234f7 - filename: injitest/src/test/java/iosTestCases/PinVcTest.java @@ -323,32 +326,30 @@ fileignoreconfig: - filename: injitest/src/test/java/iosTestCases/VcDownloadAndVerifyUsingUinTest.java checksum: a6feabb768e2d97dfb0a1693f09d839686ce6be686523cf273b2d3ce614b34fd - filename: machines/Issuers/IssuersMachine.typegen.ts - checksum: 08fd5e4eff836c219a0f16f6d4178a3511ec2581507076d3f9d32dcadbc01351 + checksum: 581e73b10471735aa6415590b0f01f6c9b503a196d3fca450a4c2a9e647487d3 - filename: injitest/src/test/java/iosTestCases/DeletingVcTest.java checksum: 0816d4b9440da5384fd867d13555986d223124b9609280350226510a06ae96fa - filename: machines/VerifiableCredential/VCItemMachine/VCItemMachine.ts checksum: fdc0c23a7107eb713d20f60fda675f9e9fe8ef29c981d798d90e581dfee340c8 checksum: 8ac74d2e5c6de179e460b86899eb048ad4c5bd67abc3d28c015e92335b8afe24 - filename: machines/VerifiableCredential/VCItemMachine/VCItemServices.ts - checksum: 1ce38602f148388940eec172a5c9be83de7a600adcae0ba9e8ac27e5ebc44641 + checksum: 9ba47d5bda1f3ebec6b5f49b7f77ea6f22a62899c9c742964495926885c53766 - filename: ios/RNPixelpassModule.m checksum: c91348eceec5edbffa03ba03f3f52a8e90ff7f942816c9609080d1647052fd66 - filename: android/app/src/main/java/io/mosip/residentapp/InjiVciClientModule.java checksum: 17f55840bab193bc353034445ba4fce53e1ce466e95f616c15a1351f8d2f23bc - filename: ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved - checksum: b168940c6b487dc96fd22f564f2e187dae46f4fa5e4a64cf81c4d810b1c1ae78 + checksum: 2d0a5899777bff2ff8412e4931e0b1087e44f63047a2e3525e82eda0dfe8791c - filename: injitest/src/main/resources/Vids.json checksum: 8bcffed7a6dd565ae695e1b29de0655e10bd5c5420af2718defd593a687b8817 - filename: injitest/src/main/java/inji/utils/UpdateNetworkSettings.java checksum: e249ce3e6b7f47abc183fe5a3637bb39ccb06900ef75b9b2f08426d1535e22aa - filename: App.tsx checksum: d16d4a40b246abe25a5d2da7ec65163b5756fe8ba9390608a7fc7f8e721b2ed1 - - filename: machines/VerifiableCredential/VCItemMachine/VCItemServices.ts - checksum: 46f5b7ad6e6dcd9de9f9872c79d2c07addcd228324a43cca18525f6b1f4ff7cb - filename: injitest/src/test/java/iosTestCases/ShareVcTest.java checksum: 1cf9b61d3fcea9b63b2b9f7dffe9b5a1848e196c39f77790b6c9d83f201c6197 - filename: android/app/src/main/java/io/mosip/residentapp/RNSecureKeystoreModule.java - checksum: f307f8273f72ec70b991baf799ae71f93c785c76e3e15847004f567558340e32 + checksum: db9d36d21607f247e2791d0ade02f2868700c432333636b0ae5a542649b69f3a - filename: injitest/src/test/java/androidTestCases/ShareVcTest.java checksum: a7e3e579b6ac05f95932638b61272142774d0690c13717c890e87374782ea509 - filename: ios/RNPixelpassModule.m @@ -364,7 +365,7 @@ fileignoreconfig: - filename: ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved checksum: b168940c6b487dc96fd22f564f2e187dae46f4fa5e4a64cf81c4d810b1c1ae78 - filename: ios/Inji.xcodeproj/project.pbxproj - checksum: 4359976ed4d1ac3206d76b87d3458d070027199c8569ba123436c4b5343aba74 + checksum: 6e83472f832f71f75aa82ed06eb677d865195755074144e4bf832d6adb30e959 - filename: screens/Settings/ReceivedCardsModal.tsx checksum: 6dee9153a61009b0252d294154c88d5e1b241a517c76e930b391a39d7bc52392 - filename: components/FaceScanner/FaceCompare.tsx @@ -372,5 +373,21 @@ fileignoreconfig: - filename: components/FaceScanner/LivenessDetection.tsx checksum: d4140a42ee9ca0f7c90e490f762d181a723fd9dd20db891cbbe53bfbd8f81632 - filename: machines/VerifiableCredential/VCItemMachine/VCItemActions.ts - checksum: 9b68ccc45681459d164197f73a1875e6f8bdf473acede18c811f4a784fca00e0 + checksum: 037ddde01479c24f954e87be5088dd0f449f0379bd8bb0b34605ca40be0f3f6c + - filename: machines/app.ts + checksum: 5da59bb384d04e29c7745d773108903fa144275c57edc1aca1898fcae7baea84 + - filename: shared/cryptoutil/signFormatConverter.ts + checksum: 3a0a03f4e719194858a61810d12180cdc407c53f04ce1455360d5d535556b7ac + - filename: shared/CloudBackupAndRestoreUtils.ts + checksum: dd98da030f7b4decb62ab2466604a2a28e6c5a1866eae9a5634b090b742497f6 + - filename: ios/RNSecureKeystoreModule.swift + checksum: 98784857098e15aca851c6eb79e48016887408ff84bb82afafdc03b38ab2e338 + - filename: machines/Issuers/IssuersModel.ts + checksum: fe4084d1f5dbf89e71cc3fbe9f845d3f2c9bb32901bd5b95a8addcb13257b414 + - filename: android/app/src/main/java/io/mosip/residentapp/InjiOpenId4VPModule.java + checksum: 6b315164dca5de95c11e0dc8cbb480207b19c312b1c9135adc39ef74a1ff7e35 + - filename: screens/Scan/SendVPScreenController.ts + checksum: f898ac7f1ecfa1df17e33b327d675f57debf2d5bd56052fc047dd03577354590 + - filename: machines/openID4VP/openID4VPMachine.typegen.ts + checksum: 986c2743bba3554068c1cf52e6f3e7b1762699e62dab6f971a6f796e2ea3eb81 version: "" diff --git a/android/app/build.gradle b/android/app/build.gradle index b73fefcb..9e8e1bda 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -262,12 +262,14 @@ android { } dependencies { + implementation("io.mosip:inji-openID4VP:0.1.0-SNAPSHOT") implementation("com.facebook.react:react-android") implementation 'com.facebook.soloader:soloader:0.10.1+' implementation("io.mosip:pixelpass:0.2.1") implementation("io.mosip:secure-keystore:0.3.0-SNAPSHOT") implementation("io.mosip:tuvali:0.5.1") implementation("io.mosip:inji-vci-client:0.2.0-SNAPSHOT") + implementation("com.google.code.gson:gson:2.10.1") implementation("io.mosip:vcverifier-aar:1.2.0-SNAPSHOT") diff --git a/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java b/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java new file mode 100644 index 00000000..411d3e32 --- /dev/null +++ b/android/app/src/main/java/io/mosip/residentapp/InjiOpenID4VPModule.java @@ -0,0 +1,122 @@ +package io.mosip.residentapp; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableMapKeySetIterator; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.mosip.openID4VP.OpenID4VP; +import io.mosip.openID4VP.dto.VPResponseMetadata; +import io.mosip.openID4VP.dto.Verifier; + +public class InjiOpenID4VPModule extends ReactContextBaseJavaModule { + private OpenID4VP openID4VP; + private Gson gson; + + InjiOpenID4VPModule(@Nullable ReactApplicationContext reactContext) { + super(reactContext); + } + + @NonNull + @Override + public String getName() { + return "InjiOpenID4VP"; + } + + @ReactMethod + public void init(String appId) { + Log.d("InjiOpenID4VPModule", "Initializing InjiOpenID4VPModule with " + appId); + openID4VP = new OpenID4VP(appId); + gson = new GsonBuilder() + .disableHtmlEscaping() + .create(); + } + + @ReactMethod + public void authenticateVerifier(String encodedAuthorizationRequest, ReadableArray trustedVerifiers, + Promise promise) { + try { + Map authenticationResponse = openID4VP.authenticateVerifier(encodedAuthorizationRequest, + convertReadableArrayToVerifierArray(trustedVerifiers)); + String authenticationResponseAsJson = gson.toJson(authenticationResponse, Map.class); + promise.resolve(authenticationResponseAsJson); + } catch (Exception exception) { + promise.reject(exception); + } + } + + @ReactMethod + public void constructVerifiablePresentationToken(ReadableMap selectedVCs, Promise promise) { + try { + Map> selectedVCsMap = new HashMap<>(); + + ReadableMapKeySetIterator iterator = selectedVCs.keySetIterator(); + while (iterator.hasNextKey()) { + String key = iterator.nextKey(); + ReadableArray valueArray = selectedVCs.getArray(key); + + List valueList = new ArrayList<>(); + for (int i = 0; i < valueArray.size(); i++) { + valueList.add(valueArray.getString(i)); + } + + selectedVCsMap.put(key, valueList); + } + String vpToken = openID4VP.constructVerifiablePresentationToken(selectedVCsMap); + promise.resolve(vpToken); + } catch (Exception exception) { + promise.reject(exception); + } + + } + + @ReactMethod + public void shareVerifiablePresentation(ReadableMap vpResponseMetadata, Promise promise) { + try { + VPResponseMetadata vpResMetadata = getVPResponseMetadata(vpResponseMetadata); + String response = openID4VP.shareVerifiablePresentation(vpResMetadata); + promise.resolve(response); + } catch (Exception exception) { + promise.reject(exception); + } + } + + private VPResponseMetadata getVPResponseMetadata(ReadableMap vpResponseMetadata) throws IllegalArgumentException { + String jws = vpResponseMetadata.getString("jws"); + String signatureAlgorithm = vpResponseMetadata.getString("signatureAlgorithm"); + String publicKey = vpResponseMetadata.getString("publicKey"); + String domain = vpResponseMetadata.getString("domain"); + + return new VPResponseMetadata(jws, signatureAlgorithm, publicKey, domain); + } + + public List convertReadableArrayToVerifierArray(ReadableArray readableArray) { + List trustedVerifiersList = new ArrayList<>(); + for (int i = 0; i < readableArray.size(); i++) { + ReadableMap verifierMap = readableArray.getMap(i); + String clientId = verifierMap.getString("client_id"); + ReadableArray responseUris = verifierMap.getArray("response_uris"); + List responseUriList = new ArrayList<>(); + for (int j = 0; j < responseUris.size(); j++) { + responseUriList.add(responseUris.getString(j)); + } + trustedVerifiersList.add(new Verifier(clientId, responseUriList)); + } + return trustedVerifiersList; + } +} diff --git a/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java b/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java index 329eb11c..02a57de5 100644 --- a/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java +++ b/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java @@ -26,6 +26,7 @@ public class InjiPackage implements ReactPackage { modules.add(new RNWalletModule(new RNEventEmitter(reactApplicationContext), new Wallet(reactApplicationContext), reactApplicationContext)); modules.add(new RNVerifierModule(new RNEventEmitter(reactApplicationContext), new Verifier(reactApplicationContext), reactApplicationContext)); modules.add(new RNQrLoginIntentModule(reactApplicationContext)); + modules.add(new InjiOpenID4VPModule(reactApplicationContext)); modules.add(new RNVCVerifierModule(reactApplicationContext)); return modules; } diff --git a/assets/Selected_Check_Box.svg b/assets/Selected_Check_Box.svg new file mode 100644 index 00000000..b9da626a --- /dev/null +++ b/assets/Selected_Check_Box.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/components/ActivityLogEvent.ts b/components/ActivityLogEvent.ts index 56c731d4..f35986bc 100644 --- a/components/ActivityLogEvent.ts +++ b/components/ActivityLogEvent.ts @@ -61,10 +61,7 @@ export class ActivityLog { export function getActionText(activity: ActivityLog, t, wellknown: Object) { if (!!activity.credentialConfigurationId) { - const cardType = getIdType( - wellknown, - activity.credentialConfigurationId, - ); + const cardType = getIdType(wellknown, activity.credentialConfigurationId); return `${t(activity.type, {idType: cardType, id: activity.id})}`; } return `${t(activity.type, {idType: '', id: activity.id})}`; diff --git a/components/FaceScanner/FaceScanner.tsx b/components/FaceScanner/FaceScanner.tsx index 4a898dee..fac61a03 100644 --- a/components/FaceScanner/FaceScanner.tsx +++ b/components/FaceScanner/FaceScanner.tsx @@ -41,7 +41,7 @@ export const FaceScanner: React.FC = props => { const {appService} = useContext(GlobalContext); const isActive = useSelector(appService, selectIsActive); - const machine = useRef(createFaceScannerMachine(props.vcImage)); + const machine = useRef(createFaceScannerMachine(props.vcImages)); const service = useInterpret(machine.current); const [cameraType, setCameraType] = useState(Camera.Constants.Type.front); @@ -118,7 +118,7 @@ export const FaceScanner: React.FC = props => { const result = await cropEyeAreaFromFace( picArray, - props.vcImage, + props.vcImages[0], faceToCompare, ); return result ? props.onValid() : props.onInvalid(); @@ -209,7 +209,7 @@ export const FaceScanner: React.FC = props => { } }; interface FaceScannerProps { - vcImage: string; + vcImages: string[]; onValid: () => void; onInvalid: () => void; isLiveness: boolean; diff --git a/components/VC/Views/VCCardView.tsx b/components/VC/Views/VCCardView.tsx index 22eb0c90..7be0d5be 100644 --- a/components/VC/Views/VCCardView.tsx +++ b/components/VC/Views/VCCardView.tsx @@ -36,14 +36,18 @@ export const VCCardView: React.FC = props => { const [wellknown, setWellknown] = useState(null); useEffect(() => { - const {issuer, wellKnown, credentialConfigurationId, vcMetadata: {format}} = - verifiableCredentialData; + const { + issuer, + wellKnown, + credentialConfigurationId, + vcMetadata: {format}, + } = verifiableCredentialData; if (wellKnown) { getCredentialIssuersWellKnownConfig( issuer, CARD_VIEW_DEFAULT_FIELDS, credentialConfigurationId, - format + format, ) .then(response => { setWellknown(response.matchingCredentialIssuerMetadata); @@ -59,7 +63,7 @@ export const VCCardView: React.FC = props => { }, [verifiableCredentialData?.wellKnown]); if (!isVCLoaded(controller.credential, fields)) { - return ; + return ; } const CardViewContent = props => ( diff --git a/components/VC/Views/VCCardViewContent.tsx b/components/VC/Views/VCCardViewContent.tsx index d8d8ff1a..377e3534 100644 --- a/components/VC/Views/VCCardViewContent.tsx +++ b/components/VC/Views/VCCardViewContent.tsx @@ -27,21 +27,37 @@ import {useCopilot} from 'react-native-copilot'; import {useTranslation} from 'react-i18next'; export const VCCardViewContent: React.FC = props => { - const isVCSelectable = props.selectable && ( - - } - uncheckedIcon={ - - } - onPress={() => props.onPress()} - /> - ); + const checkBoxForVCSharing = props.selectable && + props.flow !== VCItemContainerFlowType.OPENID4VP && ( + + } + uncheckedIcon={ + + } + onPress={() => props.onPress()} + /> + ); + const checkBoxForVPSharing = props.selectable && + props.flow === VCItemContainerFlowType.OPENID4VP && ( + + } + onPress={() => props.onPress()} + /> + ); const issuerLogo = props.verifiableCredentialData.issuerLogo; const faceImage = props.verifiableCredentialData.face; const {start} = useCopilot(); @@ -63,6 +79,7 @@ export const VCCardViewContent: React.FC = props => { : undefined }> + {checkBoxForVPSharing} {VcItemContainerProfileImage(props)} = props => { )} - {isVCSelectable} + {checkBoxForVCSharing} diff --git a/components/VC/common/VCCardSkeleton.tsx b/components/VC/common/VCCardSkeleton.tsx index 79981752..ea086721 100644 --- a/components/VC/common/VCCardSkeleton.tsx +++ b/components/VC/common/VCCardSkeleton.tsx @@ -4,9 +4,52 @@ import {ImageBackground, View} from 'react-native'; import React from 'react'; import LinearGradient from 'react-native-linear-gradient'; import ShimmerPlaceholder from 'react-native-shimmer-placeholder'; +import {VCItemContainerFlowType, VCShareFlowType} from '../../../shared/Utils'; -export const VCCardSkeleton = () => { - return ( +export const VCCardSkeleton: React.FC = props => { + return props.flow === VCItemContainerFlowType.OPENID4VP ? ( + + + + + + + + + + + + + + + + ) : ( { ); }; + +export interface VCCardSkeletonProps { + flow?: string; +} diff --git a/components/ui/svg.tsx b/components/ui/svg.tsx index e17f8b38..315aee6e 100644 --- a/components/ui/svg.tsx +++ b/components/ui/svg.tsx @@ -53,6 +53,7 @@ import ColoredInfo from '../../assets/Colored_Info.svg'; import Info from '../../assets/Info.svg'; import Search from '../../assets/Search.svg'; import CloudUploadDoneIcon from '../../assets/Cloud_Upload_Done_Icon.svg'; +import SelectedCheckBox from '../../assets/Selected_Check_Box.svg'; export class SvgImage { static MosipLogo(props: LogoProps) { @@ -64,6 +65,10 @@ export class SvgImage { return ; } + static selectedCheckBox() { + return ; + } + static walletActivatedIcon() { return ( +#import "React/RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(InjiOpenID4VP, NSObject) + +RCT_EXTERN_METHOD(init:(NSString *)appId) + +RCT_EXTERN_METHOD(authenticateVerifier:(NSString *)encodedAuthorizationRequest + trustedVerifierJSON:(id)trustedVerifierJSON + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(constructVerifiablePresentationToken:(id)credentialsMap + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(shareVerifiablePresentation:(id)vpResponseMetadata + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(requiresMainQueueSetup:(BOOL)) + +@end diff --git a/ios/RNOpenID4VPModule.swift b/ios/RNOpenID4VPModule.swift new file mode 100644 index 00000000..3cfd7a13 --- /dev/null +++ b/ios/RNOpenID4VPModule.swift @@ -0,0 +1,113 @@ +import Foundation +import OpenID4VP +import React + +@objc(InjiOpenID4VP) +class RNOpenId4VpModule: NSObject, RCTBridgeModule { + + private var openID4VP: OpenID4VP? + + static func moduleName() -> String { + return "InjiOpenID4VP" + } + + @objc + func `init`(_ appId: String) { + openID4VP = OpenID4VP(traceabilityId: appId) + } + + @objc + func authenticateVerifier(_ encodedAuthorizationRequest: String, + trustedVerifierJSON: AnyObject, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + Task { + do { + guard let verifierMeta = trustedVerifierJSON as? [[String:Any]] else { + reject("OPENID4VP", "Invalid verifier meta format", nil) + return + } + + let trustedVerifiersList: [Verifier] = try verifierMeta.map { verifierDict in + guard let clientId = verifierDict["client_id"] as? String, + let responseUris = verifierDict["response_uris"] as? [String] else { + throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid Verifier data"]) + } + return Verifier(clientId: clientId, responseUris: responseUris) + } + + let authenticationResponse: AuthenticationResponse = try await openID4VP!.authenticateVerifier(encodedAuthorizationRequest: encodedAuthorizationRequest, trustedVerifierJSON: trustedVerifiersList) + let response = try toJsonString(authenticationResponse.response) + resolve(response) + } catch { + reject("OPENID4VP", "Unable to authenticate the Verifier", error) + } + } + } + + @objc + func constructVerifiablePresentationToken(_ credentialsMap: AnyObject, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + Task { + do { + guard let credentialsMap = credentialsMap as? [String:[String]] else { + reject("OPENID4VP", "Invalid credentials map format", nil) + return + } + + let response = try await openID4VP?.constructVerifiablePresentationToken(credentialsMap: credentialsMap) + resolve(response) + + } catch { + reject("OPENID4VP","Failed to construct verifiable presentation",error) + } + } + } + + @objc + func shareVerifiablePresentation(_ vpResponseMetadata: AnyObject, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + Task { + do { + guard let vpResponse = vpResponseMetadata as? [String:String] else { + reject("OPENID4VP", "Invalid vp response meta format", nil) + return + } + + guard let jws = vpResponse["jws"] as String?, + let signatureAlgorithm = vpResponse["signatureAlgorithm"] as String?, + let publicKey = vpResponse["publicKey"] as String?, + let domain = vpResponse["domain"] as String? + else { + reject("OPENID4VP", "Invalid vp response metat", nil) + return + } + + let vpResponseMeta = VPResponseMetadata(jws: jws, signatureAlgorithm: signatureAlgorithm, publicKey: publicKey, domain: domain) + + let response = try await openID4VP?.shareVerifiablePresentation(vpResponseMetadata: vpResponseMeta) + + resolve(response) + } catch { + reject("OPENID4VP","Failed to send verifiable presentation",error) + } + } + } + +func toJsonString(_ jsonObject: Any) throws -> String { + guard let jsonDict = jsonObject as? [String: String] else { + throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid JSON object type"]) + } + + let jsonData = try JSONSerialization.data(withJSONObject: jsonDict, options: []) + guard let jsonString = String(data: jsonData, encoding: .utf8) else { + throw NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to convert data to string"]) + } + + return jsonString +} + + @objc + static func requiresMainQueueSetup() -> Bool { + return true + } + +} \ No newline at end of file diff --git a/locales/ara.json b/locales/ara.json index db670185..c0fae7d6 100644 --- a/locales/ara.json +++ b/locales/ara.json @@ -787,6 +787,51 @@ } } }, + "SendVPScreen": { + "requester": "الطالب", + "cardsSelected": "البطاقات المختارة", + "cardSelected": "card selected", + "unCheck": "البطاقة مختارة", + "checkAll": "تحقق من الكل", + "consentDialog": { + "title": "الموافقة مطلوبة", + "message": "نحن نطلب موافقتك على مشاركة بيانات الاعتماد الخاصة بك التي يمكن التحقق منها. وهذا سيمكننا من التحقق من هويتك وتلبية طلبات الخدمة الخاصة بك. اختر \"نعم، متابعة\" للموافقة أو \"رفض\" إذا كنت لا ترغب في المشاركة.", + "confirmButton": "نعم، تابع", + "cancelButton": "انخفاض" + }, + "confirmationDialog": { + "title": "هل أنت متأكد؟", + "message": "سيؤدي رفض الموافقة إلى منعك من مشاركة بيانات اعتمادك التي يمكن التحقق منها.", + "confirmButton": "نعم، تابع", + "cancelButton": "عُد" + }, + "errors": { + "noMatchingCredentials": { + "title": "لم يتم العثور على بيانات اعتماد مطابقة!", + "message": "أعد محاولة المشاركة بعد تنزيل بيانات الاعتماد." + }, + "invalidVerifier": { + "title": "حدث خطأ!", + "message": "لم يتم التعرف على المدقق. يرجى الحصول على رمز QR صالح من أداة التحقق." + }, + "credentialsMismatch": { + "title": "حدث خطأ!", + "message": "تم اكتشاف عدم تطابق في بيانات الاعتماد. تأكد من أنك قمت باختيار الخيار الصحيح ثم حاول مرة أخرى." + }, + "genericError": { + "title": "أُووبس! حدث خطأ.", + "message": "بسبب خطأ فني، لا يمكننا مشاركة البطاقة. انقر فوق إعادة المحاولة لإعادة المشاركة أو انقر فوق الصفحة الرئيسية للخروج من عملية المشاركة." + }, + "invalidQrCode": { + "title": "رمز الاستجابة السريعة غير صالح", + "message": "رمز الاستجابة السريعة غير صالح لأن بعض المعلومات المطلوبة مفقودة. يرجى مطالبة المدقق بتقديم رمز QR صالح لمشاركة بيانات الاعتماد الخاصة بك." + }, + "noImage": { + "title": "حدث خطأ!", + "message": "يتطلب التحقق من الوجه صورة في بيانات الاعتماد المحددة. الرجاء استخدام خيار المشاركة أو تحديد بيانات اعتماد تتضمن صورة." + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "التحقق من الوجه", "status": { diff --git a/locales/en.json b/locales/en.json index 555dca5e..5d5e60c7 100644 --- a/locales/en.json +++ b/locales/en.json @@ -795,6 +795,51 @@ } } }, + "SendVPScreen": { + "requester": "Requester", + "cardsSelected": "cards selected", + "cardSelected": "card selected", + "unCheck": "Uncheck", + "checkAll": "Check All", + "consentDialog": { + "title": "Consent Required", + "message": "We require your consent to share your verifiable credentials. This will enable us to verify your identity and fulfil your service requests. Choose \"Yes, Proceed\" to consent or \"Decline\" if you do not wish to share.", + "confirmButton": "Yes, Proceed", + "cancelButton": "Decline" + }, + "confirmationDialog": { + "title": "Are you sure?", + "message": "Declining the consent will prevent you from sharing your verifiable credentials.", + "confirmButton": "Yes, Proceed", + "cancelButton": "Go Back" + }, + "errors": { + "noMatchingCredentials": { + "title": "No matching credentials found!", + "message": "Retry sharing after downloading the credentials." + }, + "invalidVerifier": { + "title": "An Error Occured!", + "message": "The verifier is not recognized. Please obtain a valid QR code from the verifier." + }, + "credentialsMismatch": { + "title": "An Error Occured!", + "message": "Credential mismatch detected. Ensure you have selected the right one and try again." + }, + "genericError": { + "title": "Oops! An Error Occured.", + "message": "Due to technical error, we are unable to share the card. Click on retry to re-share or click on Home to exit the sharing process." + }, + "invalidQrCode": { + "title": "Invalid QR code", + "message": "The QR code is invalid because some required information is missing. Please ask the verifier to provide a valid QR code to share your credentials." + }, + "noImage": { + "title": "An Error Occured!", + "message": "Face verification requires a photo in the selected credential(s). Please use the Share option or select a credential that includes an image." + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "Face Verification", "status": { @@ -948,4 +993,4 @@ "keyManagementTitle": "Key Management", "keyManagementDesc": "Select the key generation method that aligns with your preference, putting you in control of your credential security.\nDrag and arrange the keys, with the top one being your highest priority." } -} +} \ No newline at end of file diff --git a/locales/fil.json b/locales/fil.json index 9acb40d7..6f1522cf 100644 --- a/locales/fil.json +++ b/locales/fil.json @@ -786,6 +786,51 @@ } } }, + "SendVPScreen": { + "requester": "Humihiling", + "cardsSelected": "mga card na napili", + "cardSelected": "card pinili", + "unCheck": "Alisin ang check", + "checkAll": "Suriin Lahat", + "consentDialog": { + "title": "Kinakailangan ang Pahintulot", + "message": "Hinihiling namin ang iyong pahintulot na ibahagi ang iyong mga nabe-verify na kredensyal. Ito ay magbibigay-daan sa amin na i-verify ang iyong pagkakakilanlan at matupad ang iyong mga kahilingan sa serbisyo. Piliin ang \"Oo, Magpatuloy\" sa pagsang-ayon o \"Tanggihan\" kung ayaw mong ibahagi.", + "confirmButton": "Oo, Magpatuloy", + "cancelButton": "Tanggihan" + }, + "confirmationDialog": { + "title": "Sigurado ka ba?", + "message": "Ang pagtanggi sa pahintulot ay mapipigilan ka sa pagbabahagi ng iyong mga nabe-verify na kredensyal.", + "confirmButton": "Oo, Magpatuloy", + "cancelButton": "Bumalik ka" + }, + "errors": { + "noMatchingCredentials": { + "title": "Walang nakitang katugmang mga kredensyal!", + "message": "Subukang muli ang pagbabahagi pagkatapos i-download ang mga kredensyal." + }, + "invalidVerifier": { + "title": "Isang Error ang Naganap!", + "message": "Hindi nakikilala ang verifier. Mangyaring kumuha ng wastong QR code mula sa verifier." + }, + "credentialsMismatch": { + "title": "Isang Error ang Naganap!", + "message": "May nakitang hindi pagkakatugma ng kredensyal. Tiyaking napili mo ang tama at subukang muli." + }, + "genericError": { + "title": "Oops! Isang Error ang Naganap.", + "message": "Dahil sa teknikal na error, hindi namin maibahagi ang card. Mag-click sa subukang muli upang muling ibahagi o mag-click sa Home upang lumabas sa proseso ng pagbabahagi." + }, + "invalidQrCode": { + "title": "Di-wastong QR code", + "message": "Di-wasto ang QR code dahil nawawala ang ilang kinakailangang impormasyon. Mangyaring hilingin sa verifier na magbigay ng wastong QR code upang ibahagi ang iyong mga kredensyal." + }, + "noImage": { + "title": "Isang Error ang Naganap!", + "message": "Ang pag-verify ng mukha ay nangangailangan ng larawan sa napiling (mga) kredensyal. Pakigamit ang opsyong Ibahagi o pumili ng kredensyal na may kasamang larawan." + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "Pagpapatunay ng Mukha", "status": { diff --git a/locales/hin.json b/locales/hin.json index 8ed2c055..a3e7776c 100644 --- a/locales/hin.json +++ b/locales/hin.json @@ -789,6 +789,51 @@ } } }, + "SendVPScreen": { + "requester": "अनुरोधकर्ता", + "cardsSelected": "कार्ड चयनित", + "cardSelected": "कार्ड चयनित", + "unCheck": "सही का निशान हटाएँ", + "checkAll": "सभी चेक करें", + "consentDialog": { + "title": "सहमति आवश्यक", + "message": "हमें आपकी सत्यापन योग्य साख साझा करने के लिए आपकी सहमति की आवश्यकता है। इससे हमें आपकी पहचान सत्यापित करने और आपके सेवा अनुरोधों को पूरा करने में मदद मिलेगी। यदि आप साझा नहीं करना चाहते हैं तो सहमति के लिए \"हां, आगे बढ़ें\" या \"अस्वीकार करें\" चुनें।", + "confirmButton": "हाँ, आगे बढ़ें", + "cancelButton": "गिरावट" + }, + "confirmationDialog": { + "title": "क्या आपको यकीन है?", + "message": "सहमति को अस्वीकार करने से आपको अपनी सत्यापन योग्य साख साझा करने से रोका जा सकेगा।", + "confirmButton": "हाँ, आगे बढ़ें", + "cancelButton": "वापस जाओ" + }, + "errors": { + "noMatchingCredentials": { + "title": "कोई मेल खाता क्रेडेंशियल नहीं मिला!", + "message": "क्रेडेंशियल डाउनलोड करने के बाद साझा करने का पुनः प्रयास करें।" + }, + "invalidVerifier": { + "title": "एक त्रुटि हुई!", + "message": "सत्यापनकर्ता को पहचाना नहीं गया है. कृपया सत्यापनकर्ता से एक वैध क्यूआर कोड प्राप्त करें।" + }, + "credentialsMismatch": { + "title": "एक त्रुटि हुई!", + "message": "क्रेडेंशियल बेमेल का पता चला. सुनिश्चित करें कि आपने सही का चयन किया है और पुनः प्रयास करें।" + }, + "genericError": { + "title": "उफ़! एक त्रुटि हुई।", + "message": "तकनीकी त्रुटि के कारण हम कार्ड साझा करने में असमर्थ हैं। पुनः साझा करने के लिए पुनः प्रयास करें पर क्लिक करें या साझाकरण प्रक्रिया से बाहर निकलने के लिए होम पर क्लिक करें।" + }, + "invalidQrCode": { + "title": "अमान्य क्यूआर कोड", + "message": "क्यूआर कोड अमान्य है क्योंकि कुछ आवश्यक जानकारी गायब है। कृपया सत्यापनकर्ता से अपने क्रेडेंशियल साझा करने के लिए एक वैध क्यूआर कोड प्रदान करने के लिए कहें।" + }, + "noImage": { + "title": "एक त्रुटि हुई!", + "message": "चेहरे के सत्यापन के लिए चयनित क्रेडेंशियल में एक फोटो की आवश्यकता होती है। कृपया शेयर विकल्प का उपयोग करें या एक क्रेडेंशियल चुनें जिसमें एक छवि शामिल हो।" + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "चेहरा सत्यापन", "status": { diff --git a/locales/kan.json b/locales/kan.json index 11d18874..0e80cd8d 100644 --- a/locales/kan.json +++ b/locales/kan.json @@ -787,6 +787,51 @@ } } }, + "SendVPScreen": { + "requester": "ವಿನಂತಿಸುವವರು", + "cardsSelected": "ಕಾರ್ಡ್‌ಗಳನ್ನು ಆಯ್ಕೆ ಮಾಡಲಾಗಿದೆ", + "cardSelected": "ಕಾರ್ಡ್ ಆಯ್ಕೆಮಾಡಲಾಗಿದೆ", + "unCheck": "ಅನ್ಚೆಕ್ ಮಾಡಿ", + "checkAll": "ಎಲ್ಲವನ್ನೂ ಪರಿಶೀಲಿಸಿ", + "consentDialog": { + "title": "ಒಪ್ಪಿಗೆ ಅಗತ್ಯವಿದೆ", + "message": "ನಿಮ್ಮ ಪರಿಶೀಲಿಸಬಹುದಾದ ರುಜುವಾತುಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ನಮಗೆ ನಿಮ್ಮ ಒಪ್ಪಿಗೆಯ ಅಗತ್ಯವಿದೆ. ಇದು ನಿಮ್ಮ ಗುರುತನ್ನು ಪರಿಶೀಲಿಸಲು ಮತ್ತು ನಿಮ್ಮ ಸೇವಾ ವಿನಂತಿಗಳನ್ನು ಪೂರೈಸಲು ನಮಗೆ ಅನುವು ಮಾಡಿಕೊಡುತ್ತದೆ. ಒಪ್ಪಿಗೆ ನೀಡಲು \"ಹೌದು, ಮುಂದುವರೆಯಿರಿ\" ಅಥವಾ ನೀವು ಹಂಚಿಕೊಳ್ಳಲು ಬಯಸದಿದ್ದರೆ \"ನಿರಾಕರಿಸಿ\" ಆಯ್ಕೆಮಾಡಿ.", + "confirmButton": "ಹೌದು, ಮುಂದುವರೆಯಿರಿ", + "cancelButton": "ನಿರಾಕರಿಸು" + }, + "confirmationDialog": { + "title": "ನೀವು ಖಚಿತವಾಗಿರುವಿರಾ?", + "message": "ಸಮ್ಮತಿಯನ್ನು ನಿರಾಕರಿಸುವುದರಿಂದ ನಿಮ್ಮ ಪರಿಶೀಲಿಸಬಹುದಾದ ರುಜುವಾತುಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳುವುದರಿಂದ ನಿಮ್ಮನ್ನು ತಡೆಯುತ್ತದೆ.", + "confirmButton": "ಹೌದು, ಮುಂದುವರೆಯಿರಿ", + "cancelButton": "ಹಿಂತಿರುಗಿ" + }, + "errors": { + "noMatchingCredentials": { + "title": "ಯಾವುದೇ ಹೊಂದಾಣಿಕೆಯ ರುಜುವಾತುಗಳು ಕಂಡುಬಂದಿಲ್ಲ!", + "message": "ರುಜುವಾತುಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿದ ನಂತರ ಹಂಚಿಕೊಳ್ಳಲು ಮರುಪ್ರಯತ್ನಿಸಿ." + }, + "invalidVerifier": { + "title": "ಒಂದು ದೋಷ ಸಂಭವಿಸಿದೆ!", + "message": "ಪರಿಶೀಲಕನನ್ನು ಗುರುತಿಸಲಾಗಿಲ್ಲ. ದಯವಿಟ್ಟು ಪರಿಶೀಲಕರಿಂದ ಮಾನ್ಯವಾದ QR ಕೋಡ್ ಅನ್ನು ಪಡೆದುಕೊಳ್ಳಿ." + }, + "credentialsMismatch": { + "title": "ಒಂದು ದೋಷ ಸಂಭವಿಸಿದೆ!", + "message": "ರುಜುವಾತು ಹೊಂದಿಕೆಯಾಗದಿರುವುದು ಪತ್ತೆಯಾಗಿದೆ. ನೀವು ಸರಿಯಾದದನ್ನು ಆರಿಸಿದ್ದೀರಿ ಎಂದು ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಿ ಮತ್ತು ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ." + }, + "genericError": { + "title": "ಓಹ್! ಒಂದು ದೋಷ ಸಂಭವಿಸಿದೆ.", + "message": "ತಾಂತ್ರಿಕ ದೋಷದಿಂದಾಗಿ, ಕಾರ್ಡ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ನಮಗೆ ಸಾಧ್ಯವಾಗುತ್ತಿಲ್ಲ. ಮರು-ಹಂಚಿಕೊಳ್ಳಲು ಮರುಪ್ರಯತ್ನದ ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ ಅಥವಾ ಹಂಚಿಕೆ ಪ್ರಕ್ರಿಯೆಯಿಂದ ನಿರ್ಗಮಿಸಲು ಹೋಮ್ ಮೇಲೆ ಕ್ಲಿಕ್ ಮಾಡಿ." + }, + "invalidQrCode": { + "title": "ಅಮಾನ್ಯವಾದ QR ಕೋಡ್", + "message": "QR ಕೋಡ್ ಅಮಾನ್ಯವಾಗಿದೆ ಏಕೆಂದರೆ ಕೆಲವು ಅಗತ್ಯ ಮಾಹಿತಿಯು ಕಾಣೆಯಾಗಿದೆ. ದಯವಿಟ್ಟು ನಿಮ್ಮ ರುಜುವಾತುಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಮಾನ್ಯವಾದ QR ಕೋಡ್ ಒದಗಿಸಲು ಪರಿಶೀಲಕರನ್ನು ಕೇಳಿ." + }, + "noImage": { + "title": "ಒಂದು ದೋಷ ಸಂಭವಿಸಿದೆ!", + "message": "ಮುಖ ಪರಿಶೀಲನೆಗೆ ಆಯ್ಕೆಮಾಡಿದ ರುಜುವಾತು(ಗಳಲ್ಲಿ) ಫೋಟೋ ಅಗತ್ಯವಿದೆ. ದಯವಿಟ್ಟು ಹಂಚಿಕೆ ಆಯ್ಕೆಯನ್ನು ಬಳಸಿ ಅಥವಾ ಚಿತ್ರವನ್ನು ಒಳಗೊಂಡಿರುವ ರುಜುವಾತುಗಳನ್ನು ಆಯ್ಕೆಮಾಡಿ." + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "ಮುಖ ಪರಿಶೀಲನೆ", "status": { diff --git a/locales/tam.json b/locales/tam.json index 0e172f2e..dc50c7eb 100644 --- a/locales/tam.json +++ b/locales/tam.json @@ -787,6 +787,51 @@ } } }, + "SendVPScreen": { + "requester": "கோரிக்கையாளர்", + "cardsSelected": "அட்டைகள் தேர்ந்தெடுக்கப்பட்டன", + "cardSelected": "அட்டை தேர்ந்தெடுக்கப்பட்டது", + "unCheck": "தேர்வுநீக்கவும்", + "checkAll": "அனைத்தையும் சரிபார்க்கவும்", + "consentDialog": { + "title": "ஒப்புதல் தேவை", + "message": "உங்களின் சரிபார்க்கக்கூடிய நற்சான்றிதழ்களைப் பகிர உங்கள் ஒப்புதல் தேவை. இது உங்கள் அடையாளத்தைச் சரிபார்க்கவும் உங்கள் சேவை கோரிக்கைகளை நிறைவேற்றவும் எங்களுக்கு உதவும். ஒப்புக்கொள்ள \"ஆம், தொடரவும்\" அல்லது நீங்கள் பகிர விரும்பவில்லை என்றால் \"நிராகரி\" என்பதைத் தேர்ந்தெடுக்கவும்.", + "confirmButton": "ஆம், தொடரவும்", + "cancelButton": "நிராகரி" + }, + "confirmationDialog": { + "title": "நீங்கள் உறுதியாக இருக்கிறீர்களா?", + "message": "ஒப்புதலை நிராகரிப்பது உங்கள் சரிபார்க்கக்கூடிய நற்சான்றிதழ்களைப் பகிர்வதைத் தடுக்கும்.", + "confirmButton": "ஆம், தொடரவும்", + "cancelButton": "திரும்பி செல்" + }, + "errors": { + "noMatchingCredentials": { + "title": "பொருந்தக்கூடிய சான்றுகள் எதுவும் கிடைக்கவில்லை!", + "message": "நற்சான்றிதழ்களைப் பதிவிறக்கிய பிறகு மீண்டும் பகிர முயற்சிக்கவும்." + }, + "invalidVerifier": { + "title": "ஒரு பிழை ஏற்பட்டது!", + "message": "சரிபார்ப்பவர் அங்கீகரிக்கப்படவில்லை. சரிபார்ப்பாளரிடமிருந்து சரியான QR குறியீட்டைப் பெறவும்." + }, + "credentialsMismatch": { + "title": "ஒரு பிழை ஏற்பட்டது!", + "message": "நற்சான்றிதழ் பொருத்தமின்மை கண்டறியப்பட்டது. நீங்கள் சரியானதைத் தேர்ந்தெடுத்துள்ளீர்கள் என்பதை உறுதிசெய்து, மீண்டும் முயற்சிக்கவும்." + }, + "genericError": { + "title": "அச்சச்சோ! ஒரு பிழை ஏற்பட்டது.", + "message": "தொழில்நுட்ப பிழை காரணமாக, கார்டை எங்களால் பகிர முடியவில்லை. மறுபகிர்வதற்கு மீண்டும் முயற்சி என்பதைக் கிளிக் செய்யவும் அல்லது பகிர்தல் செயல்முறையிலிருந்து வெளியேற முகப்பு என்பதைக் கிளிக் செய்யவும்." + }, + "invalidQrCode": { + "title": "தவறான QR குறியீடு", + "message": "தேவையான சில தகவல்கள் இல்லாததால், QR குறியீடு தவறானது. உங்கள் நற்சான்றிதழ்களைப் பகிர சரியான QR குறியீட்டை வழங்குமாறு சரிபார்ப்பாளரிடம் கேட்கவும்." + }, + "noImage": { + "title": "ஒரு பிழை ஏற்பட்டது!", + "message": "முகம் சரிபார்ப்புக்கு தேர்ந்தெடுக்கப்பட்ட நற்சான்றிதழில்(களில்) ஒரு புகைப்படம் தேவை. பகிர் விருப்பத்தைப் பயன்படுத்தவும் அல்லது படத்தை உள்ளடக்கிய நற்சான்றிதழைத் தேர்ந்தெடுக்கவும்." + } + } + }, "VerifyIdentityOverlay": { "faceAuth": "முக சரிபார்ப்பு", "status": { diff --git a/machines/Issuers/IssuersGuards.ts b/machines/Issuers/IssuersGuards.ts index 5fbc46a7..73eac668 100644 --- a/machines/Issuers/IssuersGuards.ts +++ b/machines/Issuers/IssuersGuards.ts @@ -49,7 +49,7 @@ export const IssuersGuards = () => { event.data instanceof BiometricCancellationError, isGenericError: (_: any, event: any) => { const errorMessage = event.data.message; - return !errorMessage.includes(NETWORK_REQUEST_FAILED); + return errorMessage === ErrorMessage.GENERIC; }, }; }; diff --git a/machines/Issuers/IssuersSelectors.ts b/machines/Issuers/IssuersSelectors.ts index 4bc0fd9f..2c7b8b13 100644 --- a/machines/Issuers/IssuersSelectors.ts +++ b/machines/Issuers/IssuersSelectors.ts @@ -32,7 +32,10 @@ export function selectIsBiometricCancelled(state: State) { } export function selectIsNonGenericError(state: State) { - return state.context.errorMessage !== ErrorMessage.GENERIC; + return ( + state.context.errorMessage !== ErrorMessage.GENERIC && + state.context.errorMessage !== '' + ); } export function selectIsDone(state: State) { diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemActions.ts b/machines/VerifiableCredential/VCItemMachine/VCItemActions.ts index 17cee8a9..ba0809d0 100644 --- a/machines/VerifiableCredential/VCItemMachine/VCItemActions.ts +++ b/machines/VerifiableCredential/VCItemMachine/VCItemActions.ts @@ -2,10 +2,7 @@ import {assign, send} from 'xstate'; import {CommunicationDetails} from '../../../shared/Utils'; import {StoreEvents} from '../../store'; import {VCMetadata} from '../../../shared/VCMetadata'; -import { - MIMOTO_BASE_URL, - MY_VCS_STORE_KEY, -} from '../../../shared/constants'; +import {MIMOTO_BASE_URL, MY_VCS_STORE_KEY} from '../../../shared/constants'; import i18n from '../../../i18n'; import {getHomeMachineService} from '../../../screens/Home/HomeScreenController'; import {DownloadProps} from '../../../shared/api'; @@ -459,7 +456,8 @@ export const VCItemActions = model => { type: 'VC_DOWNLOADED', id: context.vcMetadata.displayId, issuer: context.vcMetadata.issuer!!, - credentialConfigurationId: context.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.verifiableCredential.credentialConfigurationId, timestamp: Date.now(), deviceName: '', }); @@ -472,7 +470,8 @@ export const VCItemActions = model => { (context: any, _) => { const vcMetadata = VCMetadata.fromVC(context.vcMetadata); return ActivityLogEvents.LOG_ACTIVITY({ - credentialConfigurationId: context.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.verifiableCredential.credentialConfigurationId, issuer: vcMetadata.issuer!!, id: vcMetadata.displayId, _vcKey: vcMetadata.getVcKey(), @@ -491,7 +490,8 @@ export const VCItemActions = model => { return ActivityLogEvents.LOG_ACTIVITY({ _vcKey: vcMetadata.getVcKey(), type: 'WALLET_BINDING_SUCCESSFULL', - credentialConfigurationId: context.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.verifiableCredential.credentialConfigurationId, issuer: vcMetadata.issuer!!, id: vcMetadata.displayId, timestamp: Date.now(), @@ -510,7 +510,8 @@ export const VCItemActions = model => { _vcKey: vcMetadata.getVcKey(), type: 'WALLET_BINDING_FAILURE', id: vcMetadata.displayId, - credentialConfigurationId: context.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.verifiableCredential.credentialConfigurationId, issuer: vcMetadata.issuer!!, timestamp: Date.now(), deviceName: '', diff --git a/machines/VerifiableCredential/VCItemMachine/VCItemServices.ts b/machines/VerifiableCredential/VCItemMachine/VCItemServices.ts index 1009a5a0..9452b6cb 100644 --- a/machines/VerifiableCredential/VCItemMachine/VCItemServices.ts +++ b/machines/VerifiableCredential/VCItemMachine/VCItemServices.ts @@ -1,7 +1,14 @@ import {NativeModules} from 'react-native'; import Cloud from '../../../shared/CloudBackupAndRestoreUtils'; -import getAllConfigurations, {API_URLS, CACHED_API, DownloadProps,} from '../../../shared/api'; -import {fetchKeyPair, generateKeyPair,} from '../../../shared/cryptoutil/cryptoUtil'; +import getAllConfigurations, { + API_URLS, + CACHED_API, + DownloadProps, +} from '../../../shared/api'; +import { + fetchKeyPair, + generateKeyPair, +} from '../../../shared/cryptoutil/cryptoUtil'; import {CredentialDownloadResponse, request} from '../../../shared/request'; import {WalletBindingResponse} from '../VCMetaMachine/vc'; import {verifyCredential} from '../../../shared/vcjs/verifyCredential'; @@ -106,8 +113,8 @@ export const VCItemServices = model => { ); try { return getMatchingCredentialIssuerMetadata( - wellknownResponse, - context.verifiableCredential.credentialConfigurationId, + wellknownResponse, + context.verifiableCredential.credentialConfigurationId, ); } catch (error) { return {}; diff --git a/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.ts b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.ts index 7896a3ab..2f89407a 100644 --- a/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.ts +++ b/machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors.ts @@ -1,6 +1,7 @@ import {StateFrom} from 'xstate'; import {VCMetadata} from '../../../shared/VCMetadata'; import {vcMetaMachine} from './VCMetaMachine'; +import {VC} from './vc'; type State = StateFrom; @@ -19,6 +20,12 @@ export function selectShareableVcsMetadata(state: State): VCMetadata[] { ); } +export function selectShareableVcs(state: State): VC[] { + return Object.values(state.context.myVcs).filter( + vc => vc?.verifiableCredential != null, + ); +} + export function selectReceivedVcsMetadata(state: State): VCMetadata[] { return state.context.receivedVcsMetadata; } diff --git a/machines/app.typegen.ts b/machines/app.typegen.ts index e69de29b..6e90d27b 100644 --- a/machines/app.typegen.ts +++ b/machines/app.typegen.ts @@ -0,0 +1,117 @@ +// This file was automatically generated. Edits will be overwritten + +export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + 'done.invoke.app.init.checkKeyPairs:invocation[0]': { + type: 'done.invoke.app.init.checkKeyPairs:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.app.init.generateKeyPairs:invocation[0]': { + type: 'done.invoke.app.init.generateKeyPairs:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.app.ready.focus.active:invocation[0]': { + type: 'done.invoke.app.ready.focus.active:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'error.platform.app.init.checkKeyPairs:invocation[0]': { + type: 'error.platform.app.init.checkKeyPairs:invocation[0]'; + data: unknown; + }; + 'xstate.init': {type: 'xstate.init'}; + }; + invokeSrcNameMap: { + checkFocusState: 'done.invoke.app.ready.focus:invocation[0]'; + checkKeyPairs: 'done.invoke.app.init.checkKeyPairs:invocation[0]'; + checkNetworkState: 'done.invoke.app.ready.network:invocation[0]'; + generateKeyPairsAndStoreOrder: 'done.invoke.app.init.generateKeyPairs:invocation[0]'; + getAppInfo: 'done.invoke.app.init.info:invocation[0]'; + isQrLoginByDeepLink: 'done.invoke.app.ready.focus.active:invocation[0]'; + resetQRLoginDeepLinkData: 'done.invoke.app.ready.focus.active:invocation[1]'; + }; + missingImplementations: { + actions: 'forwardToServices'; + delays: never; + guards: never; + services: never; + }; + eventsCausingActions: { + forwardToServices: 'ACTIVE' | 'INACTIVE' | 'OFFLINE' | 'ONLINE'; + loadCredentialRegistryHostFromStorage: 'READY'; + loadCredentialRegistryInConstants: 'STORE_RESPONSE'; + loadEsignetHostFromConstants: 'STORE_RESPONSE'; + loadEsignetHostFromStorage: 'READY'; + logServiceEvents: 'done.invoke.app.init.checkKeyPairs:invocation[0]'; + logStoreEvents: + | 'KEY_INVALIDATE_ERROR' + | 'RESET_KEY_INVALIDATE_ERROR_DISMISS' + | 'xstate.init'; + requestDeviceInfo: 'REQUEST_DEVICE_INFO'; + resetKeyInvalidateError: 'READY' | 'RESET_KEY_INVALIDATE_ERROR_DISMISS'; + resetLinkCode: 'RESET_LINKCODE'; + setAppInfo: 'APP_INFO_RECEIVED'; + setIsDecryptError: 'DECRYPT_ERROR'; + setIsReadError: 'ERROR'; + setLinkCode: 'done.invoke.app.ready.focus.active:invocation[0]'; + spawnServiceActors: 'done.invoke.app.init.checkKeyPairs:invocation[0]'; + spawnStoreActor: + | 'KEY_INVALIDATE_ERROR' + | 'RESET_KEY_INVALIDATE_ERROR_DISMISS' + | 'xstate.init'; + unsetIsDecryptError: 'DECRYPT_ERROR_DISMISS' | 'READY'; + unsetIsReadError: 'READY'; + updateKeyInvalidateError: 'ERROR' | 'KEY_INVALIDATE_ERROR'; + }; + eventsCausingDelays: {}; + eventsCausingGuards: {}; + eventsCausingServices: { + checkFocusState: 'APP_INFO_RECEIVED'; + checkKeyPairs: + | 'READY' + | 'done.invoke.app.init.generateKeyPairs:invocation[0]'; + checkNetworkState: 'APP_INFO_RECEIVED'; + generateKeyPairsAndStoreOrder: 'error.platform.app.init.checkKeyPairs:invocation[0]'; + getAppInfo: 'STORE_RESPONSE'; + isQrLoginByDeepLink: 'ACTIVE'; + resetQRLoginDeepLinkData: 'ACTIVE'; + }; + matchesStates: + | 'init' + | 'init.checkKeyPairs' + | 'init.credentialRegistry' + | 'init.generateKeyPairs' + | 'init.info' + | 'init.services' + | 'init.store' + | 'ready' + | 'ready.focus' + | 'ready.focus.active' + | 'ready.focus.checking' + | 'ready.focus.inactive' + | 'ready.network' + | 'ready.network.checking' + | 'ready.network.offline' + | 'ready.network.online' + | 'waiting' + | { + init?: + | 'checkKeyPairs' + | 'credentialRegistry' + | 'generateKeyPairs' + | 'info' + | 'services' + | 'store'; + ready?: + | 'focus' + | 'network' + | { + focus?: 'active' | 'checking' | 'inactive'; + network?: 'checking' | 'offline' | 'online'; + }; + }; + tags: never; +} diff --git a/machines/bleShare/request/requestMachine.ts b/machines/bleShare/request/requestMachine.ts index 754f4070..6a1e7946 100644 --- a/machines/bleShare/request/requestMachine.ts +++ b/machines/bleShare/request/requestMachine.ts @@ -618,7 +618,9 @@ export const requestMachine = _vcKey: vcMetadata.getVcKey(), type: context.receiveLogType, id: vcMetadata.displayId, - credentialConfigurationId: context.incomingVc.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.incomingVc.verifiableCredential + .credentialConfigurationId, issuer: vcMetadata.issuer!!, timestamp: Date.now(), deviceName: diff --git a/machines/bleShare/request/selectors.ts b/machines/bleShare/request/selectors.ts index e06fd3d7..ca4a2ad7 100644 --- a/machines/bleShare/request/selectors.ts +++ b/machines/bleShare/request/selectors.ts @@ -30,7 +30,8 @@ export function selectVerifiableCredentialData(state: State) { getMosipLogo(), issuer: vcMetadata.issuer, wellKnown: state.context.incomingVc?.verifiableCredential?.wellKnown, - credentialConfigurationId: state.context.incomingVc?.verifiableCredential?.credentialConfigurationId, + credentialConfigurationId: + state.context.incomingVc?.verifiableCredential?.credentialConfigurationId, }; } diff --git a/machines/bleShare/scan/scanActions.ts b/machines/bleShare/scan/scanActions.ts index 373631c7..ea309009 100644 --- a/machines/bleShare/scan/scanActions.ts +++ b/machines/bleShare/scan/scanActions.ts @@ -28,13 +28,16 @@ import {ActivityLogEvents} from '../../activityLog'; import {StoreEvents} from '../../store'; import BluetoothStateManager from 'react-native-bluetooth-state-manager'; import {NativeModules} from 'react-native'; - import {wallet} from '../../../shared/tuvali'; +import {createOpenID4VPMachine} from '../../openID4VP/openID4VPMachine'; -export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { +const QR_LOGIN_REF_ID = 'QrLogin'; +const OPENID4VP_REF_ID = 'OpenID4VP'; + +export const ScanActions = (model: any) => { const {RNPixelpassModule} = NativeModules; return { - setChildRef: assign({ + setQrLoginRef: assign({ QrLoginRef: (context: any) => { const service = spawn( createQrLoginMachine(context.serviceRefs), @@ -45,6 +48,17 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { }, }), + setOpenId4VPRef: assign({ + OpenId4VPRef: (context: any) => { + const service = spawn( + createOpenID4VPMachine(context.serviceRefs), + OPENID4VP_REF_ID, + ); + service.subscribe(logState); + return service; + }, + }), + resetLinkCode: model.assign({ linkcode: '', }), @@ -85,6 +99,14 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { selectedVc: context.selectedVc, }), + sendVPScanData: context => + context.OpenId4VPRef.send({ + type: 'AUTHENTICATE', + encodedAuthRequest: context.linkCode, + flowType: context.openID4VPFlowType, + selectedVC: context.selectedVc, + }), + openBluetoothSettings: () => { isAndroid() ? BluetoothStateManager.openSettings().catch() @@ -145,10 +167,28 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { flowType: (_context, event) => event.flowType, }), + setOpenId4VPFlowType: assign({ + openID4VPFlowType: (context: any) => { + let flowType = VCShareFlowType.OPENID4VP; + if (context.flowType === VCShareFlowType.MINI_VIEW_SHARE) { + flowType = VCShareFlowType.MINI_VIEW_SHARE_OPENID4VP; + } else if ( + context.flowType === VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE + ) { + flowType = VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP; + } + return flowType; + }, + }), + resetFlowType: assign({ flowType: VCShareFlowType.SIMPLE_SHARE, }), + resetOpenID4VPFlowType: assign({ + openID4VPFlowType: '', + }), + registerLoggers: assign({ loggers: () => { if (__DEV__) { @@ -200,7 +240,8 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { ? context.shareLogType : 'VC_SHARED_WITH_VERIFICATION_CONSENT', id: vcMetadata.displayId, - credentialConfigurationId: context.selectedVc.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.selectedVc.verifiableCredential.credentialConfigurationId, issuer: vcMetadata.issuer!!, timestamp: Date.now(), deviceName: @@ -217,7 +258,8 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { _vcKey: vcMetadata.getVcKey(), type: 'PRESENCE_VERIFICATION_FAILED', timestamp: Date.now(), - credentialConfigurationId: context.selectedVc.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + context.selectedVc.verifiableCredential.credentialConfigurationId, id: vcMetadata.displayId, issuer: vcMetadata.issuer!!, deviceName: @@ -228,8 +270,10 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { ), setLinkCode: assign({ - linkCode: (_, event) => - new URL(event.params).searchParams.get('linkCode'), + linkCode: (context: any, event) => + context.openID4VPFlowType.startsWith('OpenID4VP') + ? event.params + : new URL(event.params).searchParams.get('linkCode'), }), setLinkCodeFromDeepLink: assign({ @@ -282,7 +326,8 @@ export const ScanActions = (model: any, QR_LOGIN_REF_ID: any) => { _vcKey: '', id: vcMetadata.displayId, issuer: vcMetadata.issuer!!, - credentialConfigurationId: selectedVc.verifiableCredential.credentialConfigurationId, + credentialConfigurationId: + selectedVc.verifiableCredential.credentialConfigurationId, type: 'QRLOGIN_SUCCESFULL', timestamp: Date.now(), deviceName: '', diff --git a/machines/bleShare/scan/scanGuards.ts b/machines/bleShare/scan/scanGuards.ts index 5bedf4c3..6643ebee 100644 --- a/machines/bleShare/scan/scanGuards.ts +++ b/machines/bleShare/scan/scanGuards.ts @@ -24,6 +24,10 @@ export const ScanGuards = () => { } }, + isOnlineSharing: (_, event) => { + return event.params.startsWith('openid4vp://authorize'); + }, + uptoAndroid11: () => isAndroid() && androidVersion < 31, isIOS: () => isIOS(), diff --git a/machines/bleShare/scan/scanMachine.ts b/machines/bleShare/scan/scanMachine.ts index f32e370d..033d9f93 100644 --- a/machines/bleShare/scan/scanMachine.ts +++ b/machines/bleShare/scan/scanMachine.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/no-duplicate-string */ -import {EventFrom, send, StateFrom} from 'xstate'; +import {actions, EventFrom, send, StateFrom} from 'xstate'; import {AppServices} from '../../../shared/GlobalContext'; import {TelemetryConstants} from '../../../shared/telemetry/TelemetryConstants'; import { @@ -12,9 +12,9 @@ import {ScanActions} from './scanActions'; import {ScanGuards} from './scanGuards'; import {ScanModel} from './scanModel'; import {ScanServices} from './scanServices'; +import {openID4VPMachine} from '../../openID4VP/openID4VPMachine'; const model = ScanModel; -const QR_LOGIN_REF_ID = 'QrLogin'; export const ScanEvents = model.events; export const scanMachine = @@ -35,6 +35,7 @@ export const scanMachine = initial: 'inactive', on: { SCREEN_BLUR: { + actions: 'resetOpenID4VPFlowType', target: '#scan.disconnectDevice', }, SCREEN_FOCUS: { @@ -75,6 +76,7 @@ export const scanMachine = }, }, checkStorage: { + entry: 'setOpenId4VPRef', invoke: { src: 'checkStorageAvailability', onDone: [ @@ -319,7 +321,6 @@ export const scanMachine = 'removeLoggers', 'registerLoggers', 'clearUri', - 'setChildRef', 'resetFaceCaptureBannerStatus', ], on: { @@ -338,7 +339,16 @@ export const scanMachine = { target: 'showQrLogin', cond: 'isQrLogin', - actions: ['sendVcSharingStartEvent', 'setLinkCode'], + actions: [ + 'setQrLoginRef', + 'sendVcSharingStartEvent', + 'setLinkCode', + ], + }, + { + target: 'startVPSharing', + cond: 'isOnlineSharing', + actions: ['setOpenId4VPFlowType', 'setLinkCode'], }, { target: 'decodeQuickShareData', @@ -351,6 +361,89 @@ export const scanMachine = ], }, }, + startVPSharing: { + entry: [ + 'sendVPScanData', + () => + sendStartEvent( + getStartEventData(TelemetryConstants.FlowType.vpSharing), + ), + ], + invoke: { + id: 'OpenId4VP', + src: openID4VPMachine, + onDone: {}, + }, + on: { + IN_PROGRESS: { + target: '.inProgress', + }, + TIMEOUT: { + target: '.timeout', + }, + DISMISS: [ + { + cond: 'isFlowTypeSimpleShare', + actions: 'resetOpenID4VPFlowType', + target: 'checkStorage', + }, + { + target: 'checkStorage', + }, + ], + SHOW_ERROR: { + target: '.showError', + }, + SUCCESS: { + target: '.success', + }, + }, + states: { + success: {}, + showError: {}, + inProgress: { + on: { + CANCEL: [ + { + cond: 'isFlowTypeSimpleShare', + actions: 'resetOpenID4VPFlowType', + target: '#scan.checkStorage', + }, + { + target: '#scan.checkStorage', + }, + ], + }, + }, + timeout: { + on: { + STAY_IN_PROGRESS: { + target: 'inProgress', + }, + CANCEL: [ + { + cond: 'isFlowTypeSimpleShare', + actions: 'resetOpenID4VPFlowType', + target: '#scan.checkStorage', + }, + { + target: '#scan.checkStorage', + }, + ], + RETRY: [ + { + cond: 'isFlowTypeSimpleShare', + actions: 'resetOpenID4VPFlowType', + target: '#scan.checkStorage', + }, + { + target: '#scan.checkStorage', + }, + ], + }, + }, + }, + }, decodeQuickShareData: { entry: 'loadMetaDataToMemory', on: { @@ -781,10 +874,8 @@ export const scanMachine = }, }, { - actions: ScanActions(model, QR_LOGIN_REF_ID), - + actions: ScanActions(model), services: ScanServices(model), - guards: ScanGuards(), delays: { DESTROY_TIMEOUT: 500, diff --git a/machines/bleShare/scan/scanMachine.typegen.ts b/machines/bleShare/scan/scanMachine.typegen.ts index 1b39b24b..a2de269f 100644 --- a/machines/bleShare/scan/scanMachine.typegen.ts +++ b/machines/bleShare/scan/scanMachine.typegen.ts @@ -57,12 +57,14 @@ export interface Typegen0 { | 'resetFaceCaptureBannerStatus' | 'resetFlowType' | 'resetLinkCode' + | 'resetOpenID4VPFlowType' | 'resetSelectedVc' | 'resetShowQuickShareSuccessBanner' | 'sendBLEConnectionErrorEvent' | 'sendScanData' | 'sendVCShareFlowCancelEndEvent' | 'sendVCShareFlowTimeoutEndEvent' + | 'sendVPScanData' | 'sendVcShareSuccessEvent' | 'sendVcSharingStartEvent' | 'setBleError' @@ -70,6 +72,9 @@ export interface Typegen0 { | 'setFlowType' | 'setLinkCode' | 'setLinkCodeFromDeepLink' + | 'setOpenId4VPFlowType' + | 'setOpenId4VPRef' + | 'setQrLoginRef' | 'setQuickShareData' | 'setReadyForBluetoothStateCheck' | 'setReceiverInfo' @@ -92,6 +97,7 @@ export interface Typegen0 { | 'isFlowTypeSimpleShare' | 'isIOS' | 'isMinimumStorageRequiredForAuditEntryReached' + | 'isOnlineSharing' | 'isOpenIdQr' | 'isQrLogin' | 'isQuickShare' @@ -156,6 +162,7 @@ export interface Typegen0 { | 'SCREEN_FOCUS' | 'SELECT_VC' | 'xstate.stop'; + resetOpenID4VPFlowType: 'CANCEL' | 'DISMISS' | 'RETRY' | 'SCREEN_BLUR'; resetSelectedVc: | 'DISCONNECT' | 'DISMISS' @@ -169,13 +176,23 @@ export interface Typegen0 { sendScanData: 'QRLOGIN_VIA_DEEP_LINK' | 'SCAN'; sendVCShareFlowCancelEndEvent: 'CANCEL'; sendVCShareFlowTimeoutEndEvent: 'CANCEL' | 'RETRY'; + sendVPScanData: 'SCAN'; sendVcShareSuccessEvent: 'VC_ACCEPTED'; sendVcSharingStartEvent: 'SCAN'; setBleError: 'BLE_ERROR'; - setChildRef: 'QRLOGIN_VIA_DEEP_LINK' | 'STORE_RESPONSE'; + setChildRef: 'QRLOGIN_VIA_DEEP_LINK'; setFlowType: 'SELECT_VC'; setLinkCode: 'SCAN'; setLinkCodeFromDeepLink: 'QRLOGIN_VIA_DEEP_LINK'; + setOpenId4VPFlowType: 'SCAN'; + setOpenId4VPRef: + | 'CANCEL' + | 'DISMISS' + | 'RESET' + | 'RETRY' + | 'SCREEN_FOCUS' + | 'SELECT_VC'; + setQrLoginRef: 'SCAN'; setQuickShareData: 'SCAN'; setReadyForBluetoothStateCheck: 'BLUETOOTH_PERMISSION_ENABLED'; setReceiverInfo: 'CONNECTED'; @@ -200,9 +217,10 @@ export interface Typegen0 { eventsCausingGuards: { isFlowTypeMiniViewShare: 'CHECK_FLOW_TYPE'; isFlowTypeMiniViewShareWithSelfie: 'CHECK_FLOW_TYPE' | 'DISMISS'; - isFlowTypeSimpleShare: 'CANCEL' | 'CHECK_FLOW_TYPE' | 'DISMISS'; + isFlowTypeSimpleShare: 'CANCEL' | 'CHECK_FLOW_TYPE' | 'DISMISS' | 'RETRY'; isIOS: 'BLUETOOTH_STATE_DISABLED' | 'START_PERMISSION_CHECK'; isMinimumStorageRequiredForAuditEntryReached: 'done.invoke.scan.checkStorage:invocation[0]'; + isOnlineSharing: 'SCAN'; isOpenIdQr: 'SCAN'; isQrLogin: 'SCAN'; isQuickShare: 'SCAN'; @@ -210,6 +228,7 @@ export interface Typegen0 { uptoAndroid11: '' | 'START_PERMISSION_CHECK'; }; eventsCausingServices: { + OpenId4VP: 'SCAN'; QrLogin: 'QRLOGIN_VIA_DEEP_LINK' | 'SCAN'; checkBluetoothPermission: | '' @@ -220,7 +239,13 @@ export interface Typegen0 { checkLocationPermission: 'LOCATION_ENABLED'; checkLocationStatus: '' | 'APP_ACTIVE' | 'LOCATION_REQUEST'; checkNearByDevicesPermission: 'APP_ACTIVE' | 'START_PERMISSION_CHECK'; - checkStorageAvailability: 'RESET' | 'SCREEN_FOCUS' | 'SELECT_VC'; + checkStorageAvailability: + | 'CANCEL' + | 'DISMISS' + | 'RESET' + | 'RETRY' + | 'SCREEN_FOCUS' + | 'SELECT_VC'; disconnect: '' | 'DISMISS' | 'LOCATION_ENABLED' | 'RETRY' | 'SCREEN_BLUR'; monitorConnection: 'DISMISS' | 'SCREEN_BLUR' | 'xstate.init'; requestBluetooth: 'BLUETOOTH_STATE_DISABLED'; @@ -293,6 +318,11 @@ export interface Typegen0 { | 'showQrLogin.navigatingToHistory' | 'showQrLogin.storing' | 'startPermissionCheck' + | 'startVPSharing' + | 'startVPSharing.inProgress' + | 'startVPSharing.showError' + | 'startVPSharing.success' + | 'startVPSharing.timeout' | { checkBluetoothPermission?: 'checking' | 'enabled'; checkBluetoothState?: 'checking' | 'enabled' | 'requesting'; @@ -322,6 +352,7 @@ export interface Typegen0 { | 'verifyingIdentity' | {sendingVc?: 'inProgress' | 'sent' | 'timeout'}; showQrLogin?: 'idle' | 'navigatingToHistory' | 'storing'; + startVPSharing?: 'inProgress' | 'showError' | 'success' | 'timeout'; }; tags: never; } diff --git a/machines/bleShare/scan/scanModel.ts b/machines/bleShare/scan/scanModel.ts index 0b3e27b1..93b96f97 100644 --- a/machines/bleShare/scan/scanModel.ts +++ b/machines/bleShare/scan/scanModel.ts @@ -9,6 +9,7 @@ import {qrLoginMachine} from '../../QrLogin/QrLoginMachine'; import {VC} from '../../VerifiableCredential/VCMetaMachine/vc'; import {ActivityLogType} from '../../activityLog'; import {BLEError} from '../types'; +import {openID4VPMachine} from '../../openID4VP/openID4VPMachine'; const ScanEvents = { SELECT_VC: (vc: VC, flowType: string) => ({vc, flowType}), @@ -55,6 +56,10 @@ const ScanEvents = { }), ALLOWED: () => ({}), DENIED: () => ({}), + SHOW_ERROR: () => ({}), + SUCCESS: () => ({}), + IN_PROGRESS: () => ({}), + TIMEOUT: () => ({}), QRLOGIN_VIA_DEEP_LINK: (linkCode: string) => ({linkCode}), }; @@ -68,10 +73,12 @@ export const ScanModel = createModel( loggers: [] as EmitterSubscription[], vcName: '', flowType: VCShareFlowType.SIMPLE_SHARE, + openID4VPFlowType: '', verificationImage: {} as CameraCapturedPicture, openId4VpUri: '', shareLogType: '' as ActivityLogType, QrLoginRef: {} as ActorRefFrom, + OpenId4VPRef: {} as ActorRefFrom, showQuickShareSuccessBanner: false, linkCode: '', quickShareData: {}, diff --git a/machines/bleShare/scan/scanSelectors.ts b/machines/bleShare/scan/scanSelectors.ts index 5f6a2e80..75282042 100644 --- a/machines/bleShare/scan/scanSelectors.ts +++ b/machines/bleShare/scan/scanSelectors.ts @@ -9,6 +9,10 @@ export function selectFlowType(state: State) { return state.context.flowType; } +export function selectOpenID4VPFlowType(state: State) { + return state.context.openID4VPFlowType; +} + export function selectReceiverInfo(state: State) { return state.context.receiverInfo; } @@ -18,26 +22,28 @@ export function selectVcName(state: State) { } export function selectCredential(state: State) { - return ( + return [ state.context.selectedVc?.verifiableCredential?.credential || - state.context.selectedVc?.verifiableCredential - ); + state.context.selectedVc?.verifiableCredential, + ]; } export function selectVerifiableCredentialData(state: State) { const vcMetadata = new VCMetadata(state.context.selectedVc?.vcMetadata); - return { - vcMetadata: vcMetadata, - issuer: vcMetadata.issuer, - issuerLogo: - state.context.selectedVc?.verifiableCredential?.issuerLogo || - getMosipLogo(), - face: - state.context.selectedVc?.verifiableCredential?.credential - ?.credentialSubject?.face || - state.context.selectedVc?.credential?.biometrics?.face, - wellKnown: state.context.selectedVc?.verifiableCredential?.wellKnown, - }; + return [ + { + vcMetadata: vcMetadata, + issuer: vcMetadata.issuer, + issuerLogo: + state.context.selectedVc?.verifiableCredential?.issuerLogo || + getMosipLogo(), + face: + state.context.selectedVc?.verifiableCredential?.credential + ?.credentialSubject?.face || + state.context.selectedVc?.credential?.biometrics?.face, + wellKnown: state.context.selectedVc?.verifiableCredential?.wellKnown, + }, + ]; } export function selectQrLoginRef(state: State) { @@ -71,6 +77,18 @@ export function selectIsSendingVc(state: State) { return state.matches('reviewing.sendingVc.inProgress'); } +export function selectIsSendingVP(state: State) { + return state.matches('startVPSharing.inProgress'); +} + +export function selectIsSendingVPError(state: State) { + return state.matches('startVPSharing.showError'); +} + +export function selectIsSendingVPSuccess(state: State) { + return state.matches('startVPSharing.success'); +} + export function selectIsFaceIdentityVerified(state: State) { return ( state.matches('reviewing.sendingVc.inProgress') && @@ -82,6 +100,10 @@ export function selectIsSendingVcTimeout(state: State) { return state.matches('reviewing.sendingVc.timeout'); } +export function selectIsSendingVPTimeout(state: State) { + return state.matches('startVPSharing.timeout'); +} + export function selectIsSent(state: State) { return state.matches('reviewing.sendingVc.sent'); } diff --git a/machines/faceScanner.ts b/machines/faceScanner.ts index 21a01e4b..74068f7f 100644 --- a/machines/faceScanner.ts +++ b/machines/faceScanner.ts @@ -30,7 +30,7 @@ const model = createModel( export const FaceScannerEvents = model.events; -export const createFaceScannerMachine = (vcImage: string) => +export const createFaceScannerMachine = (vcImages: string[]) => /** @xstate-layout N4IgpgJg5mDOIC5QDMCGBjMBldqB2eYATgHQCWeZALiegBZjoDWFUACsQLZmyxkD2eAMQARAKIA5AJJiRiUAAd+fKgLzyQAD0QBaAIwAmAyQMBWAJwBmABzWADHoAslywfOOANCACeu0wHYANhJ-R389SzC3QJjLAF84rzRMHHxCUgpqWgZmVg4ibl41IQBxACUAQQkAFVkNJRU1DW0EHUc7YxtA6z09c2sza3NzQK9fVtdjO0s7fqHrAL1-U0CEpIxsXAJickoaBS4ePkERMEpIIQB5NkkAfSwxauqpCRKseuVqJqQtXW6Q6aGbrWGymIzWMa6AwzEiOCwrZwzbp6QL+NYgZKbNI7TL7Q5FE5nMgXCpsNi3ABilwAwgBVB5yH4NL6CZpQ-zGazhQKmUwdSIGVGQiZGEjTWahIYGOx2azOdGY1LbDJ7EhEMAARwArnBVHh2PjjsJylVaozFJ9VKyfi03I4TJFHI4YqYFkEucL9JY9LC9GDxX7zAYQQqNkr0rssurtbq8obiuJpHUmZbvqAWkt7Y5g45ek7A4EnZ7JmLvQ4ZbKrI5zGjEhiw1sI7ihGUxBURABND6Na3pvzBfwclzWQs80y9CE+RBTexcwWy8eFlyhlKNnawRusEiYin8LV4CBCCCCMC7ABu-CYp8Va9IG7SW53e4PCAoF9wVrwAG07ABdbssuoNq6JY5gkN6gSGCiIJ6HYoTLMKrrGMssH2P4lj+HYzp6CuWLKiQ94EI+Gy7vuh7EEQ-CkAoAA2qBUMgVGcNuDbYnem76ixmCkS+b78B+ag-v+KY9kBfYINKpgkCMrpOg42auIEljCu0PqymEMyyoEBi5qsdY3mxBEcVAQgUgAMlI5LUhUACyYiVABn5sggegLCE-gDCCBiDjWIL+MKM6yhygQLiiiK4eG67GaZFTUmItziLU1Jmo5aa-BMIUkNpMzOOYQUYaMU4SWY0lBiM3qWK6qKOAkdZ4PwEBwBoBn4bi2SMCw+r5IURqpb26VtHYwQYUOSlWEYNieiF1ghGEhg1g47jLvprGtaqBwFEcainOcEB9WJA3mD6fIVQYfozNKLjFsGJCmDMIxLTl47xCtq6GW1G09WoJREPgVCQPtzmDfaGEYQMvJOpYSmehE-jSX6rmQdM7gRNYEW3pGNDRjqsB6gam0EgdzJOcBLkeSE0odIO-go3B-lFTorqWCYc2RA4yxBAY6PvXsgOk20MIjdCY2TJNDPIrN4TBpB0puOY3P4YRlCcU+ZF8+JjMzbYjhcjW7TmHyQTCjYM2yehMojjpKIKxGSvEZgEj8FQPF7SJgFA5l2aonK4Rgk43mmAF2awmVmGVa5zpwjbUUPvq6sDTY9rZVhoH5UbRVOHYpXRNmZgebMXOvXhTacKgMDUqgChUFq6quxaolA+0YoxAjbgjrYRjCuhUlmO0Osophzq1usb3KvHLRtKYIODsLoGi5O4w6Ms9rwYYyxGL0ha1XEQA */ model.createMachine( { @@ -198,12 +198,34 @@ export const createFaceScannerMachine = (vcImage: string) => }); }, - verifyImage: context => { + verifyImage: async context => { context.cameraRef.pausePreview(); const rxDataURI = /data:(?[\w/\-.]+);(?\w+),(?.*)/; - const matches = rxDataURI.exec(vcImage).groups; - return faceCompare(context.capturedImage.base64, matches.data); + + let isMatchFound = false; + for (const vcImage of vcImages) { + const matches = rxDataURI.exec(vcImage).groups; + + try { + isMatchFound = await faceCompare( + context.capturedImage.base64, + matches.data, + ); + if (isMatchFound) { + break; + } + } catch (error) { + throw error; + } + } + + return isMatchFound; + }, + + checkNetworkStatus: async () => { + const state = await NetInfo.fetch(); + return state.isConnected; }, }, diff --git a/machines/faceScanner.typegen.ts b/machines/faceScanner.typegen.ts index b7e60e89..27baba94 100644 --- a/machines/faceScanner.typegen.ts +++ b/machines/faceScanner.typegen.ts @@ -32,7 +32,6 @@ export interface Typegen0 { services: never; }; eventsCausingActions: { - flipWhichCamera: 'FLIP_CAMERA'; openSettings: 'OPEN_SETTINGS'; setCameraRef: 'READY'; setCaptureError: 'error.platform.faceScanner.capturing:invocation[0]'; diff --git a/machines/openID4VP/openID4VPActions.ts b/machines/openID4VP/openID4VPActions.ts new file mode 100644 index 00000000..d5132a56 --- /dev/null +++ b/machines/openID4VP/openID4VPActions.ts @@ -0,0 +1,193 @@ +import {assign} from 'xstate'; +import {send, sendParent} from 'xstate/lib/actions'; +import {SHOW_FACE_AUTH_CONSENT_SHARE_FLOW} from '../../shared/constants'; +import {VC} from '../VerifiableCredential/VCMetaMachine/vc'; +import {StoreEvents} from '../store'; + +import {VCShareFlowType} from '../../shared/Utils'; + +// TODO - get this presentation definition list which are alias for scope param +// from the verifier end point after the endpoint is created and exposed. + +export const openID4VPActions = (model: any) => { + return { + setAuthenticationResponse: model.assign({ + authenticationResponse: (_, event) => event.data, + }), + + setEncodedAuthorizationRequest: model.assign({ + encodedAuthorizationRequest: (_, event) => event.encodedAuthRequest, + }), + + setFlowType: model.assign({ + flowType: (_, event) => event.flowType, + }), + + getVcsMatchingAuthRequest: model.assign({ + vcsMatchingAuthRequest: (context, event) => { + let vcs = event.vcs; + let matchingVCs = {} as Record; + let presentationDefinition; + const response = context.authenticationResponse; + if ('presentation_definition' in response) { + presentationDefinition = JSON.parse( + response['presentation_definition'], + ); + } + vcs.forEach(vc => { + presentationDefinition['input_descriptors'].forEach( + inputDescriptor => { + let isMatched = true; + inputDescriptor.constraints.fields?.forEach(field => { + field.path.forEach(path => { + const pathSegments = path.substring(2).split('.'); + + const pathData = pathSegments.reduce( + (obj, key) => obj?.[key], + vc.verifiableCredential.credential, + ); + + if ( + path === undefined || + (pathSegments[pathSegments.length - 1] !== 'type' && + (field.filter?.type !== typeof pathData || + !pathData.includes(field.filter?.pattern))) + ) { + isMatched = false; + return; + } + }); + + if (!isMatched) { + return; + } + }); + + if (isMatched) { + matchingVCs[inputDescriptor.id]?.push(vc) || + (matchingVCs[inputDescriptor.id] = [vc]); + } + }, + ); + }); + return matchingVCs; + }, + purpose: context => { + const response = context.authenticationResponse; + if ('presentation_definition' in response) { + const pd = JSON.parse(response['presentation_definition']); + return pd.purpose ?? ''; + } + }, + }), + + setSelectedVCs: model.assign({ + selectedVCs: (_, event) => event.selectedVCs, + }), + + compareAndStoreSelectedVC: model.assign({ + selectedVCs: context => { + const matchingVcs = {}; + Object.entries(context.vcsMatchingAuthRequest).map( + ([inputDescriptorId, vcs]) => + (vcs as VC[]).map(vcData => { + if ( + vcData.vcMetadata.requestId === + context.miniViewSelectedVC.vcMetadata.requestId + ) { + matchingVcs[inputDescriptorId] = [vcData]; + } + }), + ); + return matchingVcs; + }, + }), + + setMiniViewShareSelectedVC: model.assign({ + miniViewSelectedVC: (_, event) => event.selectedVC, + }), + + setIsShareWithSelfie: model.assign({ + isShareWithSelfie: (_, event) => + event.flowType === + VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP, + }), + + setShowFaceAuthConsent: model.assign({ + showFaceAuthConsent: (_, event) => { + return !event.isDoNotAskAgainChecked; + }, + }), + + storeShowFaceAuthConsent: send( + (_, event) => + StoreEvents.SET( + SHOW_FACE_AUTH_CONSENT_SHARE_FLOW, + !event.isDoNotAskAgainChecked, + ), + { + to: context => context.serviceRefs.store, + }, + ), + + getFaceAuthConsent: send( + StoreEvents.GET(SHOW_FACE_AUTH_CONSENT_SHARE_FLOW), + { + to: (context: any) => context.serviceRefs.store, + }, + ), + + updateShowFaceAuthConsent: model.assign({ + showFaceAuthConsent: (_, event) => { + return event.response || event.response === null; + }, + }), + + forwardToParent: sendParent('DISMISS'), + + setError: model.assign({ + error: (_, event) => { + console.error('Error:', event.data.message); + return event.data.message; + }, + }), + + resetError: model.assign({ + error: () => '', + }), + + loadKeyPair: assign({ + publicKey: (_, event: any) => event.data?.publicKey as string, + privateKey: (context: any, event: any) => + event.data?.privateKey + ? event.data.privateKey + : (context.privateKey as string), + }), + + incrementOpenID4VPRetryCount: model.assign({ + openID4VPRetryCount: context => context.openID4VPRetryCount + 1, + }), + + resetOpenID4VPRetryCount: model.assign({ + openID4VPRetryCount: () => 0, + }), + + setAuthenticationError: model.assign({ + error: (_, event) => { + console.error('Error:', event.data.message); + return 'vc validation - ' + event.data.message; + }, + }), + + setTrustedVerifiersApiCallError: model.assign({ + error: (_, event) => { + console.error('Error:', event.data.message); + return 'api error - ' + event.data.message; + }, + }), + + setTrustedVerifiers: model.assign({ + trustedVerifiers: (_: any, event: any) => event.data.response.verifiers, + }), + }; +}; diff --git a/machines/openID4VP/openID4VPGuards.ts b/machines/openID4VP/openID4VPGuards.ts new file mode 100644 index 00000000..a373ecd6 --- /dev/null +++ b/machines/openID4VP/openID4VPGuards.ts @@ -0,0 +1,33 @@ +import {VCShareFlowType} from '../../shared/Utils'; + +export const openID4VPGuards = () => { + return { + showFaceAuthConsentScreen: (context, event) => { + return context.showFaceAuthConsent && context.isShareWithSelfie; + }, + + isShareWithSelfie: context => context.isShareWithSelfie, + + isSimpleOpenID4VPShare: context => + context.flowType === VCShareFlowType.OPENID4VP, + + isSelectedVCMatchingRequest: context => + Object.values(context.selectedVCs).length === 1, + + isFlowTypeSimpleShare: context => + context.flowType === VCShareFlowType.SIMPLE_SHARE, + + hasKeyPair: (context: any) => { + return !!context.publicKey; + }, + + isAnyVCHasImage: (context: any) => { + const hasImage = Object.values(context.selectedVCs) + .flatMap(vc => vc) + .some( + vc => vc.verifiableCredential?.credential?.credentialSubject.face, + ); + return !!hasImage; + }, + }; +}; diff --git a/machines/openID4VP/openID4VPMachine.ts b/machines/openID4VP/openID4VPMachine.ts new file mode 100644 index 00000000..a9f8ab4b --- /dev/null +++ b/machines/openID4VP/openID4VPMachine.ts @@ -0,0 +1,335 @@ +import {EventFrom} from 'xstate'; +import {openID4VPModel} from './openID4VPModel'; +import {openID4VPServices} from './openID4VPServices'; +import {openID4VPActions} from './openID4VPActions'; +import {AppServices} from '../../shared/GlobalContext'; +import {openID4VPGuards} from './openID4VPGuards'; +import {send, sendParent} from 'xstate/lib/actions'; + +const model = openID4VPModel; + +export const OpenID4VPEvents = model.events; + +export const openID4VPMachine = model.createMachine( + { + predictableActionArguments: true, + preserveActionOrder: true, + tsTypes: {} as import('./openID4VPMachine.typegen').Typegen0, + schema: { + context: model.initialContext, + events: {} as EventFrom, + }, + id: 'OpenID4VP', + initial: 'waitingForData', + + states: { + waitingForData: { + on: { + AUTHENTICATE: { + actions: [ + 'setEncodedAuthorizationRequest', + 'setFlowType', + 'setMiniViewShareSelectedVC', + 'setIsShareWithSelfie', + ], + target: 'checkFaceAuthConsent', + }, + }, + }, + checkFaceAuthConsent: { + entry: 'getFaceAuthConsent', + on: { + STORE_RESPONSE: { + actions: 'updateShowFaceAuthConsent', + target: 'getTrustedVerifiersList', + }, + }, + }, + getTrustedVerifiersList: { + invoke: { + src: 'fetchTrustedVerifiers', + onDone: { + actions: 'setTrustedVerifiers', + target: 'getKeyPairFromKeystore', + }, + onError: { + actions: 'setTrustedVerifiersApiCallError', + }, + }, + }, + getKeyPairFromKeystore: { + invoke: { + src: 'getKeyPair', + onDone: { + actions: ['loadKeyPair'], + target: 'checkKeyPair', + }, + onError: [ + { + actions: 'setError', + }, + ], + }, + }, + checkKeyPair: { + description: 'checks whether key pair is generated', + invoke: { + src: 'getSelectedKey', + onDone: { + cond: 'hasKeyPair', + target: 'authenticateVerifier', + }, + onError: [ + { + actions: 'setError', + }, + ], + }, + }, + authenticateVerifier: { + invoke: { + src: 'getAuthenticationResponse', + onDone: { + actions: 'setAuthenticationResponse', + target: 'getVCsSatisfyingAuthRequest', + }, + onError: { + actions: 'setAuthenticationError', + target: 'showError', + }, + }, + }, + getVCsSatisfyingAuthRequest: { + on: { + DOWNLOADED_VCS: [ + { + cond: 'isSimpleOpenID4VPShare', + actions: 'getVcsMatchingAuthRequest', + target: 'selectingVCs', + }, + { + actions: 'getVcsMatchingAuthRequest', + target: 'setSelectedVC', + }, + ], + }, + }, + setSelectedVC: { + entry: send('SET_SELECTED_VC'), + on: { + SET_SELECTED_VC: [ + { + actions: 'compareAndStoreSelectedVC', + target: 'checkIfMatchingVCsHasSelectedVC', + }, + ], + }, + }, + checkIfMatchingVCsHasSelectedVC: { + entry: send('CHECK_SELECTED_VC'), + on: { + CHECK_SELECTED_VC: [ + { + cond: 'isSelectedVCMatchingRequest', + target: 'getConsentForVPSharing', + }, + { + actions: [ + model.assign({ + error: () => 'credential mismatch detected', + }), + ], + target: 'showError', + }, + ], + }, + }, + selectingVCs: { + on: { + VERIFY_AND_ACCEPT_REQUEST: { + actions: [ + 'setSelectedVCs', + model.assign({isShareWithSelfie: () => true}), + ], + target: 'getConsentForVPSharing', + }, + ACCEPT_REQUEST: { + target: 'getConsentForVPSharing', + actions: [ + 'setSelectedVCs', + 'setShareLogTypeUnverified', + 'resetFaceCaptureBannerStatus', + ], + }, + CANCEL: { + actions: 'forwardToParent', + target: 'waitingForData', + }, + }, + }, + getConsentForVPSharing: { + on: { + CONFIRM: [ + { + cond: 'showFaceAuthConsentScreen', + target: 'faceVerificationConsent', + }, + { + cond: 'isShareWithSelfie', + target: 'checkIfAnySelectedVCHasImage', + }, + { + target: 'sendingVP', + }, + ], + CANCEL: { + target: 'showConfirmationPopup', + }, + }, + }, + showConfirmationPopup: { + on: { + CONFIRM: { + actions: sendParent('DISMISS'), + }, + GO_BACK: { + target: 'getConsentForVPSharing', + }, + }, + }, + faceVerificationConsent: { + on: { + FACE_VERIFICATION_CONSENT: [ + { + cond: 'isSimpleOpenID4VPShare', + actions: ['setShowFaceAuthConsent', 'storeShowFaceAuthConsent'], + target: 'checkIfAnySelectedVCHasImage', + }, + { + actions: ['setShowFaceAuthConsent', 'storeShowFaceAuthConsent'], + target: 'verifyingIdentity', + }, + ], + DISMISS: [ + { + cond: 'isSimpleOpenID4VPShare', + target: 'selectingVCs', + }, + { + actions: sendParent('DISMISS'), + }, + ], + }, + }, + checkIfAnySelectedVCHasImage: { + entry: send('CHECK_FOR_IMAGE'), + on: { + CHECK_FOR_IMAGE: [ + { + cond: 'isAnyVCHasImage', + target: 'verifyingIdentity', + }, + { + actions: model.assign({ + error: () => 'none of the selected VC has image', + }), + target: 'showError', + }, + ], + }, + }, + verifyingIdentity: { + on: { + FACE_VALID: [ + { + cond: 'hasKeyPair', + target: 'sendingVP', + }, + { + target: 'checkKeyPair', + }, + ], + FACE_INVALID: { + target: 'invalidIdentity', + actions: 'logFailedVerification', + }, + CANCEL: [ + { + cond: 'isSimpleOpenID4VPShare', + actions: model.assign({isShareWithSelfie: () => false}), + target: 'selectingVCs', + }, + { + actions: sendParent('DISMISS'), + }, + ], + }, + }, + invalidIdentity: { + on: { + DISMISS: [ + { + cond: 'isSimpleOpenID4VPShare', + target: 'selectingVCs', + }, + { + actions: sendParent('DISMISS'), + }, + ], + RETRY_VERIFICATION: { + target: 'verifyingIdentity', + }, + }, + }, + sendingVP: { + entry: sendParent('IN_PROGRESS'), + invoke: { + src: 'sendVP', + onDone: { + actions: sendParent('SUCCESS'), + target: 'success', + }, + onError: { + actions: ['setError', sendParent('SHOW_ERROR')], + target: 'showError', + }, + }, + after: { + SHARING_TIMEOUT: { + actions: sendParent('TIMEOUT'), + }, + }, + }, + showError: { + on: { + RETRY: { + actions: ['incrementOpenID4VPRetryCount'], + target: 'sendingVP', + }, + RESET_RETRY_COUNT: { + actions: 'resetOpenID4VPRetryCount', + }, + RESET_ERROR: { + actions: 'resetError', + }, + }, + }, + success: {}, + }, + }, + { + actions: openID4VPActions(model), + services: openID4VPServices(), + guards: openID4VPGuards(), + delays: { + SHARING_TIMEOUT: 15 * 1000, + }, + }, +); + +export function createOpenID4VPMachine(serviceRefs: AppServices) { + return openID4VPMachine.withContext({ + ...openID4VPMachine.context, + serviceRefs, + }); +} diff --git a/machines/openID4VP/openID4VPMachine.typegen.ts b/machines/openID4VP/openID4VPMachine.typegen.ts new file mode 100644 index 00000000..d5b76606 --- /dev/null +++ b/machines/openID4VP/openID4VPMachine.typegen.ts @@ -0,0 +1,177 @@ +// This file was automatically generated. Edits will be overwritten + +export interface Typegen0 { + '@@xstate/typegen': true; + internalEvents: { + 'done.invoke.OpenID4VP.authenticateVerifier:invocation[0]': { + type: 'done.invoke.OpenID4VP.authenticateVerifier:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]': { + type: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]': { + type: 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]': { + type: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.OpenID4VP.sendingVP:invocation[0]': { + type: 'done.invoke.OpenID4VP.sendingVP:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'error.platform.OpenID4VP.authenticateVerifier:invocation[0]': { + type: 'error.platform.OpenID4VP.authenticateVerifier:invocation[0]'; + data: unknown; + }; + 'error.platform.OpenID4VP.checkKeyPair:invocation[0]': { + type: 'error.platform.OpenID4VP.checkKeyPair:invocation[0]'; + data: unknown; + }; + 'error.platform.OpenID4VP.getKeyPairFromKeystore:invocation[0]': { + type: 'error.platform.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; + data: unknown; + }; + 'error.platform.OpenID4VP.getTrustedVerifiersList:invocation[0]': { + type: 'error.platform.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + data: unknown; + }; + 'error.platform.OpenID4VP.sendingVP:invocation[0]': { + type: 'error.platform.OpenID4VP.sendingVP:invocation[0]'; + data: unknown; + }; + 'xstate.init': {type: 'xstate.init'}; + }; + invokeSrcNameMap: { + fetchTrustedVerifiers: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + getAuthenticationResponse: 'done.invoke.OpenID4VP.authenticateVerifier:invocation[0]'; + getKeyPair: 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; + getSelectedKey: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; + sendVP: 'done.invoke.OpenID4VP.sendingVP:invocation[0]'; + }; + missingImplementations: { + actions: + | 'compareAndStoreSelectedVC' + | 'forwardToParent' + | 'getFaceAuthConsent' + | 'getVcsMatchingAuthRequest' + | 'incrementOpenID4VPRetryCount' + | 'loadKeyPair' + | 'logFailedVerification' + | 'resetError' + | 'resetFaceCaptureBannerStatus' + | 'resetOpenID4VPRetryCount' + | 'setAuthenticationError' + | 'setAuthenticationResponse' + | 'setEncodedAuthorizationRequest' + | 'setError' + | 'setFlowType' + | 'setIsShareWithSelfie' + | 'setMiniViewShareSelectedVC' + | 'setSelectedVCs' + | 'setShareLogTypeUnverified' + | 'setShowFaceAuthConsent' + | 'setTrustedVerifiers' + | 'setTrustedVerifiersApiCallError' + | 'storeShowFaceAuthConsent' + | 'updateShowFaceAuthConsent'; + delays: never; + guards: + | 'hasKeyPair' + | 'isAnyVCHasImage' + | 'isSelectedVCMatchingRequest' + | 'isShareWithSelfie' + | 'isSimpleOpenID4VPShare' + | 'showFaceAuthConsentScreen'; + services: + | 'fetchTrustedVerifiers' + | 'getAuthenticationResponse' + | 'getKeyPair' + | 'getSelectedKey' + | 'sendVP'; + }; + eventsCausingActions: { + compareAndStoreSelectedVC: 'SET_SELECTED_VC'; + forwardToParent: 'CANCEL'; + getFaceAuthConsent: 'AUTHENTICATE'; + getVcsMatchingAuthRequest: 'DOWNLOADED_VCS'; + incrementOpenID4VPRetryCount: 'RETRY'; + loadKeyPair: 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; + logFailedVerification: 'FACE_INVALID'; + resetError: 'RESET_ERROR'; + resetFaceCaptureBannerStatus: 'ACCEPT_REQUEST'; + resetOpenID4VPRetryCount: 'RESET_RETRY_COUNT'; + setAuthenticationError: 'error.platform.OpenID4VP.authenticateVerifier:invocation[0]'; + setAuthenticationResponse: 'done.invoke.OpenID4VP.authenticateVerifier:invocation[0]'; + setEncodedAuthorizationRequest: 'AUTHENTICATE'; + setError: + | 'error.platform.OpenID4VP.checkKeyPair:invocation[0]' + | 'error.platform.OpenID4VP.getKeyPairFromKeystore:invocation[0]' + | 'error.platform.OpenID4VP.sendingVP:invocation[0]'; + setFlowType: 'AUTHENTICATE'; + setIsShareWithSelfie: 'AUTHENTICATE'; + setMiniViewShareSelectedVC: 'AUTHENTICATE'; + setSelectedVCs: 'ACCEPT_REQUEST' | 'VERIFY_AND_ACCEPT_REQUEST'; + setShareLogTypeUnverified: 'ACCEPT_REQUEST'; + setShowFaceAuthConsent: 'FACE_VERIFICATION_CONSENT'; + setTrustedVerifiers: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + setTrustedVerifiersApiCallError: 'error.platform.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + storeShowFaceAuthConsent: 'FACE_VERIFICATION_CONSENT'; + updateShowFaceAuthConsent: 'STORE_RESPONSE'; + }; + eventsCausingDelays: { + SHARING_TIMEOUT: 'CONFIRM' | 'FACE_VALID' | 'RETRY'; + }; + eventsCausingGuards: { + hasKeyPair: + | 'FACE_VALID' + | 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; + isAnyVCHasImage: 'CHECK_FOR_IMAGE'; + isSelectedVCMatchingRequest: 'CHECK_SELECTED_VC'; + isShareWithSelfie: 'CONFIRM'; + isSimpleOpenID4VPShare: + | 'CANCEL' + | 'DISMISS' + | 'DOWNLOADED_VCS' + | 'FACE_VERIFICATION_CONSENT'; + showFaceAuthConsentScreen: 'CONFIRM'; + }; + eventsCausingServices: { + fetchTrustedVerifiers: 'STORE_RESPONSE'; + getAuthenticationResponse: 'done.invoke.OpenID4VP.checkKeyPair:invocation[0]'; + getKeyPair: 'done.invoke.OpenID4VP.getTrustedVerifiersList:invocation[0]'; + getSelectedKey: + | 'FACE_VALID' + | 'done.invoke.OpenID4VP.getKeyPairFromKeystore:invocation[0]'; + sendVP: 'CONFIRM' | 'FACE_VALID' | 'RETRY'; + }; + matchesStates: + | 'authenticateVerifier' + | 'checkFaceAuthConsent' + | 'checkIfAnySelectedVCHasImage' + | 'checkIfMatchingVCsHasSelectedVC' + | 'checkKeyPair' + | 'faceVerificationConsent' + | 'getConsentForVPSharing' + | 'getKeyPairFromKeystore' + | 'getTrustedVerifiersList' + | 'getVCsSatisfyingAuthRequest' + | 'invalidIdentity' + | 'selectingVCs' + | 'sendingVP' + | 'setSelectedVC' + | 'showConfirmationPopup' + | 'showError' + | 'success' + | 'verifyingIdentity' + | 'waitingForData'; + tags: never; +} diff --git a/machines/openID4VP/openID4VPModel.ts b/machines/openID4VP/openID4VPModel.ts new file mode 100644 index 00000000..0b02939d --- /dev/null +++ b/machines/openID4VP/openID4VPModel.ts @@ -0,0 +1,68 @@ +import {createModel} from 'xstate/lib/model'; +import {AppServices} from '../../shared/GlobalContext'; +import {VC} from '../VerifiableCredential/VCMetaMachine/vc'; +import {KeyTypes} from '../../shared/cryptoutil/KeyTypes'; +const openID4VPEvents = { + AUTHENTICATE: ( + encodedAuthRequest: string, + flowType: string, + selectedVC: any, + ) => ({encodedAuthRequest, flowType, selectedVC}), + DOWNLOADED_VCS: (vcs: VC[]) => ({vcs}), + SELECT_VC: (vcKey: string, inputDescriptorId: any) => ({ + vcKey, + inputDescriptorId, + }), + ACCEPT_REQUEST: (selectedVCs: Record) => ({ + selectedVCs, + }), + VERIFY_AND_ACCEPT_REQUEST: (selectedVCs: Record) => ({ + selectedVCs, + }), + CONFIRM: () => ({}), + CANCEL: () => ({}), + FACE_VERIFICATION_CONSENT: (isDoNotAskAgainChecked: boolean) => ({ + isDoNotAskAgainChecked, + }), + FACE_VALID: () => ({}), + FACE_INVALID: () => ({}), + DISMISS: () => ({}), + RETRY_VERIFICATION: () => ({}), + STORE_RESPONSE: (response: any) => ({response}), + GO_BACK: () => ({}), + CHECK_SELECTED_VC: () => ({}), + SET_SELECTED_VC: () => ({}), + CHECK_FOR_IMAGE: () => ({}), + RETRY: () => ({}), + RESET_RETRY_COUNT: () => ({}), + RESET_ERROR: () => ({}), +}; + +export const openID4VPModel = createModel( + { + serviceRefs: {} as AppServices, + encodedAuthorizationRequest: '' as string, + authenticationResponse: {}, + vcsMatchingAuthRequest: {} as Record, + checkedAll: false as boolean, + selectedVCs: {} as Record, + isShareWithSelfie: false as boolean, + showFaceAuthConsent: true as boolean, + purpose: '' as string, + error: '' as string, + publicKey: '', + privateKey: '', + keyType: KeyTypes.RS256, + flowType: '' as string, + miniViewSelectedVC: {} as VC, + openID4VPRetryCount: 0, + trustedVerifiers: [] as VerifierType[], + }, + {events: openID4VPEvents}, +); + +interface VerifierType { + client_id: string; + redirect_uri: string; + response_uri: string; +} diff --git a/machines/openID4VP/openID4VPSelectors.ts b/machines/openID4VP/openID4VPSelectors.ts new file mode 100644 index 00000000..e50191c7 --- /dev/null +++ b/machines/openID4VP/openID4VPSelectors.ts @@ -0,0 +1,96 @@ +import {StateFrom} from 'xstate'; +import {openID4VPMachine} from './openID4VPMachine'; +import {VCMetadata} from '../../shared/VCMetadata'; +import {getMosipLogo} from '../../components/VC/common/VCUtils'; +import {VerifiableCredentialData} from '../VerifiableCredential/VCMetaMachine/vc'; + +type State = StateFrom; + +export function selectIsGetVCsSatisfyingAuthRequest(state: State) { + return state.matches('getVCsSatisfyingAuthRequest'); +} + +export function selectVCsMatchingAuthRequest(state: State) { + return state.context.vcsMatchingAuthRequest; +} + +export function selectSelectedVCs(state: State) { + return state.context.selectedVCs; +} + +export function selectAreAllVCsChecked(state: State) { + return state.context.checkedAll; +} + +export function selectIsGetVPSharingConsent(state: State) { + return state.matches('getConsentForVPSharing'); +} + +export function selectIsFaceVerificationConsent(state: State) { + return state.matches('faceVerificationConsent'); +} + +export function selectIsVerifyingIdentity(state: State) { + return state.matches('verifyingIdentity'); +} + +export function selectIsInvalidIdentity(state: State) { + return state.matches('invalidIdentity'); +} + +export function selectIsSharingVP(state: State) { + return state.matches('sendingVP'); +} + +export function selectCredentials(state: State) { + let selectedCredentials: Credential[] = []; + Object.values(state.context.selectedVCs).map(vcs => { + vcs.map(vcData => { + const credential = + vcData?.verifiableCredential?.credential || + vcData?.verifiableCredential; + selectedCredentials.push(credential); + }); + }); + return selectedCredentials; +} + +export function selectVerifiableCredentialsData(state: State) { + let verifiableCredentialsData: VerifiableCredentialData[] = []; + Object.values(state.context.selectedVCs).map(vcs => { + vcs.map(vcData => { + const vcMetadata = new VCMetadata(vcData.vcMetadata); + verifiableCredentialsData.push({ + vcMetadata: vcMetadata, + issuer: vcMetadata.issuer, + issuerLogo: vcData?.verifiableCredential?.issuerLogo || getMosipLogo(), + face: + vcData?.verifiableCredential?.credential?.credentialSubject?.face || + vcData?.credential?.biometrics?.face, + wellKnown: vcData?.verifiableCredential?.wellKnown, + credentialTypes: vcData?.verifiableCredential?.credentialTypes, + }); + }); + }); + return verifiableCredentialsData; +} + +export function selectPurpose(state: State) { + return state.context.purpose; +} + +export function selectShowConfirmationPopup(state: State) { + return state.matches('showConfirmationPopup'); +} + +export function selectIsSelectingVcs(state: State) { + return state.matches('selectingVCs'); +} + +export function selectIsError(state: State) { + return state.context.error; +} + +export function selectOpenID4VPRetryCount(state: State) { + return state.context.openID4VPRetryCount; +} diff --git a/machines/openID4VP/openID4VPServices.ts b/machines/openID4VP/openID4VPServices.ts new file mode 100644 index 00000000..1c8b729d --- /dev/null +++ b/machines/openID4VP/openID4VPServices.ts @@ -0,0 +1,56 @@ +import {CACHED_API} from '../../shared/api'; +import {fetchKeyPair} from '../../shared/cryptoutil/cryptoUtil'; +import { + constructProofJWT, + OpenID4VP, + OpenID4VP_Domain, + OpenID4VP_Proof_Algo_Type, +} from '../../shared/openID4VP/OpenID4VP'; + +export const openID4VPServices = () => { + return { + fetchTrustedVerifiers: async () => { + return await CACHED_API.fetchTrustedVerifiersList(); + }, + + getAuthenticationResponse: (context: any) => async () => { + OpenID4VP.initialize(); + const serviceRes = await OpenID4VP.authenticateVerifier( + context.encodedAuthorizationRequest, + context.trustedVerifiers, + ); + return serviceRes; + }, + + getKeyPair: async (context: any) => { + if (!!(await fetchKeyPair(context.keyType)).publicKey) { + return await fetchKeyPair(context.keyType); + } + }, + + getSelectedKey: async (context: any) => { + return await fetchKeyPair(context.keyType); + }, + + sendVP: (context: any) => async () => { + const vpToken = await OpenID4VP.constructVerifiablePresentationToken( + context.selectedVCs, + ); + + const proofJWT = await constructProofJWT( + context.publicKey, + context.privateKey, + JSON.parse(vpToken), + context.keyType, + ); + + const vpResponseMetadata = { + jws: proofJWT, + signatureAlgorithm: OpenID4VP_Proof_Algo_Type, + publicKey: context.publicKey, + domain: OpenID4VP_Domain, + }; + return await OpenID4VP.shareVerifiablePresentation(vpResponseMetadata); + }, + }; +}; diff --git a/routes/routesConstants.ts b/routes/routesConstants.ts index e4854398..3e83ed0d 100644 --- a/routes/routesConstants.ts +++ b/routes/routesConstants.ts @@ -10,6 +10,7 @@ export const BOTTOM_TAB_ROUTES = { export const SCAN_ROUTES = { ScanScreen: 'ScanScreen' as keyof ScanStackParamList, SendVcScreen: 'SendVcScreen' as keyof ScanStackParamList, + SendVPScreen: 'SendVPScreen' as keyof ScanStackParamList, }; export const REQUEST_ROUTES = { @@ -25,6 +26,7 @@ export const SETTINGS_ROUTES = { export type ScanStackParamList = { ScanScreen: undefined; SendVcScreen: undefined; + SendVPScreen: undefined; }; export type RequestStackParamList = { diff --git a/screens/Request/ReceiveVcScreen.tsx b/screens/Request/ReceiveVcScreen.tsx index b4c1cb60..bd0a20a7 100644 --- a/screens/Request/ReceiveVcScreen.tsx +++ b/screens/Request/ReceiveVcScreen.tsx @@ -1,16 +1,16 @@ -import React, { useEffect, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { DeviceInfoList } from '../../components/DeviceInfoList'; -import { Button, Column, Text } from '../../components/ui'; -import { Theme } from '../../components/ui/styleUtils'; -import { useReceiveVcScreen } from './ReceiveVcScreenController'; -import { MessageOverlay } from '../../components/MessageOverlay'; -import { useOverlayVisibleAfterTimeout } from '../../shared/hooks/useOverlayVisibleAfterTimeout'; -import { VcDetailsContainer } from '../../components/VC/VcDetailsContainer'; -import { SharingStatusModal } from '../Scan/SharingStatusModal'; -import { SvgImage } from '../../components/ui/svg'; -import { DETAIL_VIEW_DEFAULT_FIELDS } from '../../components/VC/common/VCUtils'; -import { getDetailedViewFields } from '../../shared/openId4VCI/Utils'; +import React, {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import {DeviceInfoList} from '../../components/DeviceInfoList'; +import {Button, Column, Text} from '../../components/ui'; +import {Theme} from '../../components/ui/styleUtils'; +import {useReceiveVcScreen} from './ReceiveVcScreenController'; +import {MessageOverlay} from '../../components/MessageOverlay'; +import {useOverlayVisibleAfterTimeout} from '../../shared/hooks/useOverlayVisibleAfterTimeout'; +import {VcDetailsContainer} from '../../components/VC/VcDetailsContainer'; +import {SharingStatusModal} from '../Scan/SharingStatusModal'; +import {SvgImage} from '../../components/ui/svg'; +import {DETAIL_VIEW_DEFAULT_FIELDS} from '../../components/VC/common/VCUtils'; +import {getDetailedViewFields} from '../../shared/openId4VCI/Utils'; export const ReceiveVcScreen: React.FC = () => { const {t} = useTranslation('ReceiveVcScreen'); diff --git a/screens/Scan/ScanLayout.tsx b/screens/Scan/ScanLayout.tsx index b54e6dfc..96bd59b8 100644 --- a/screens/Scan/ScanLayout.tsx +++ b/screens/Scan/ScanLayout.tsx @@ -16,13 +16,13 @@ import {View, I18nManager} from 'react-native'; import {Text} from './../../components/ui'; import {BannerStatusType} from '../../components/BannerNotification'; import {LIVENESS_CHECK} from '../../shared/constants'; +import {SendVPScreen} from './SendVPScreen'; const ScanStack = createNativeStackNavigator(); export const ScanLayout: React.FC = () => { const {t} = useTranslation('ScanScreen'); const controller = useScanLayout(); - if ( controller.statusOverlay != null && !controller.isAccepted && @@ -37,7 +37,8 @@ export const ScanLayout: React.FC = () => { isHintVisible={ controller.isStayInProgress || controller.isBleError || - controller.isSendingVc + controller.isSendingVc || + controller.isSendingVP } onRetry={controller.statusOverlay?.onRetry} showBanner={controller.isFaceIdentityVerified} @@ -113,10 +114,43 @@ export const ScanLayout: React.FC = () => { ), }} /> + {controller.openID4VPFlowType === VCShareFlowType.OPENID4VP && ( + ( + + + {props.children} + + + ), + headerBackVisible: false, + headerRight: () => + !I18nManager.isRTL && ( + + ), + headerLeft: () => + I18nManager.isRTL && ( + + ), + }} + /> + )} { const {t} = useTranslation('ScanScreen'); const scanScreenController = useScanScreen(); const sendVcScreenController = useSendVcScreen(); + const sendVPScreenController = useSendVPScreen(); const [isBluetoothOn, setIsBluetoothOn] = useState(false); + const showErrorModal = + sendVPScreenController.scanScreenError || + (sendVPScreenController.errorModal.show && + (sendVPScreenController.flowType === + VCShareFlowType.MINI_VIEW_SHARE_OPENID4VP || + sendVPScreenController.flowType === + VCShareFlowType.MINI_VIEW_SHARE_WITH_SELFIE_OPENID4VP)); useEffect(() => { (async () => { @@ -54,6 +67,11 @@ export const ScanScreen: React.FC = () => { Linking.openSettings(); }; + const handleTextButtonEvent = () => { + sendVPScreenController.GO_TO_HOME(); + sendVPScreenController.RESET_RETRY_COUNT(); + }; + function noShareableVcText() { return ( { ); } + const faceVerificationController = sendVPScreenController.flowType.startsWith( + 'OpenID4VP', + ) + ? sendVPScreenController + : sendVcScreenController; + return ( + { /> {displayStorageLimitReachedError()} + + {sendVPScreenController.flowType.startsWith('OpenID4VP') && + sendVPScreenController.flowType !== VCShareFlowType.OPENID4VP && + sendVPScreenController.overlayDetails !== null && ( + + )} + <> + + + + ); }; diff --git a/screens/Scan/SendVPScreen.tsx b/screens/Scan/SendVPScreen.tsx new file mode 100644 index 00000000..07c18fc5 --- /dev/null +++ b/screens/Scan/SendVPScreen.tsx @@ -0,0 +1,247 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React, {useEffect} from 'react'; +import {useTranslation} from 'react-i18next'; +import {BackHandler, View} from 'react-native'; +import {Button, Column, Row, Text} from '../../components/ui'; +import {Theme} from '../../components/ui/styleUtils'; +import {VcItemContainer} from '../../components/VC/VcItemContainer'; +import {LIVENESS_CHECK} from '../../shared/constants'; +import {TelemetryConstants} from '../../shared/telemetry/TelemetryConstants'; +import { + getImpressionEventData, + sendImpressionEvent, +} from '../../shared/telemetry/TelemetryUtils'; +import {isMosipVC, VCItemContainerFlowType} from '../../shared/Utils'; +import {VCMetadata} from '../../shared/VCMetadata'; +import {VerifyIdentityOverlay} from '../VerifyIdentityOverlay'; +import {VPShareOverlay} from './VPShareOverlay'; +import {FaceVerificationAlertOverlay} from './FaceVerificationAlertOverlay'; +import {useSendVPScreen} from './SendVPScreenController'; +import LinearGradient from 'react-native-linear-gradient'; +import {Error} from '../../components/ui/Error'; +import {SvgImage} from '../../components/ui/svg'; + +export const SendVPScreen: React.FC = () => { + const {t} = useTranslation('SendVPScreen'); + const controller = useSendVPScreen(); + + const vcsMatchingAuthRequest = controller.vcsMatchingAuthRequest; + + useEffect(() => { + sendImpressionEvent( + getImpressionEventData( + TelemetryConstants.FlowType.senderVcShare, + TelemetryConstants.Screens.vcList, + ), + ); + }, []); + + useFocusEffect( + React.useCallback(() => { + const onBackPress = () => true; + + const disableBackHandler = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress, + ); + + return () => disableBackHandler.remove(); + }, []), + ); + + const handleTextButtonEvent = () => { + controller.GO_TO_HOME(); + controller.RESET_RETRY_COUNT(); + }; + + const getVcKey = vcData => { + return VCMetadata.fromVcMetadataString(vcData.vcMetadata).getVcKey(); + }; + + const noOfCardsSelected = controller.areAllVCsChecked + ? Object.values(controller.vcsMatchingAuthRequest).length + : Object.keys(controller.selectedVCKeys).length; + + const cardsSelectedText = + noOfCardsSelected === 1 + ? noOfCardsSelected + ' ' + t('cardSelected') + : noOfCardsSelected + ' ' + t('cardsSelected'); + + const areAllVcsChecked = + noOfCardsSelected === + Object.values(controller.vcsMatchingAuthRequest).flatMap(vc => vc).length; + + return ( + + {Object.keys(vcsMatchingAuthRequest).length > 0 && ( + <> + {controller.purpose !== '' && ( + + + + {controller.purpose} + + + + )} + + + + + {t('SendVcScreen:pleaseSelectAnId')} + + + + + + {cardsSelectedText} + + + {areAllVcsChecked ? t('unCheck') : t('checkAll')} + + + + {Object.entries(vcsMatchingAuthRequest).map( + ([inputDescriptorId, vcs]) => + vcs.map(vcData => ( + + )), + )} + + +