From a7d1b9adbfdc747d51e7360c3bf4478f2a37160f Mon Sep 17 00:00:00 2001 From: balachandarg-tw Date: Tue, 16 Sep 2025 20:58:16 +0530 Subject: [PATCH] [INJIMOB-956]: SVG rendering (#2080) * [INJIMOB-956]: Android native module integration Signed-off-by: balachandarg-tw * [INJIMOB-956]: iOS native module integration and react native layer Signed-off-by: balachandarg-tw * [INJIMOB-956]: React native UI design changes Signed-off-by: balachandarg-tw * [INJIMOB-3499]: Updating android native module integration to adapt latest chagnes in libray api Signed-off-by: balachandarg-tw * [INJIMOB-3499]: Updated Native module integration Signed-off-by: balachandarg-tw * [INJIMOB-956]: Update tht package dependency version Signed-off-by: balachandarg-tw * [INJIMOB-956]: Loading before SVg rendering issue fixed Signed-off-by: balachandarg-tw * [INJIMOB-956]: show Qr code button based on fallback image id Signed-off-by: balachandarg-tw * [INJIMOB-956]: Update Swift package dependency Signed-off-by: balachandarg-tw * [INJIMOB-3499]: Update Swift Package dependency to develop of Renderer library Signed-off-by: balachandarg-tw * [INJIMOB-956]: Update in received screen Signed-off-by: balachandarg-tw * [INJIMOB-956]: Update format in renderVC call Signed-off-by: balachandarg-tw * [INJIMOB-956]: Change ordering of the params Signed-off-by: balachandarg-tw * [INJIMOB-956]: Changes in caching Signed-off-by: balachandarg-tw * [INJIMOB-956]: Updating the package dependency files Signed-off-by: balachandarg-tw --------- Signed-off-by: balachandarg-tw --- .talismanrc | 8 +- android/app/build.gradle | 1 + .../io/mosip/residentapp/InjiPackage.java | 1 + .../residentapp/RNInjiVcRendererModule.java | 66 ++++ components/QrCodeOverlay.tsx | 51 +-- components/VC/Views/VCDetailView.tsx | 309 ++++++++++++------ ios/Inji.xcodeproj/project.pbxproj | 25 ++ .../xcshareddata/swiftpm/Package.resolved | 11 +- ios/RNInjiVcRenderer.m | 16 + ios/RNInjiVcRenderer.swift | 57 ++++ screens/Home/ViewVcModal.tsx | 55 +++- screens/Request/ReceiveVcScreen.tsx | 39 +++ shared/Utils.ts | 6 +- shared/constants.ts | 2 + shared/vcRenderer/VcRenderer.ts | 86 +++++ 15 files changed, 593 insertions(+), 140 deletions(-) create mode 100644 android/app/src/main/java/io/mosip/residentapp/RNInjiVcRendererModule.java create mode 100644 ios/RNInjiVcRenderer.m create mode 100644 ios/RNInjiVcRenderer.swift create mode 100644 shared/vcRenderer/VcRenderer.ts diff --git a/.talismanrc b/.talismanrc index 513fccff..c0a66b7e 100644 --- a/.talismanrc +++ b/.talismanrc @@ -341,7 +341,7 @@ fileignoreconfig: - filename: android/app/src/main/java/io/mosip/residentapp/InjiVciClientModule.java checksum: 17f55840bab193bc353034445ba4fce53e1ce466e95f616c15a1351f8d2f23bc - filename: ios/Inji.xcworkspace/xcshareddata/swiftpm/Package.resolved - checksum: 3cedf13dd287307d7ed29958a65127919cc3e47293c1959a91388329451dbf1e + checksum: 252427dd3d91cc71d644c0448e07683e3b7f3ff3936b31b075691c1f1cd0ba0d - filename: injitest/src/main/resources/Vids.json checksum: 8bcffed7a6dd565ae695e1b29de0655e10bd5c5420af2718defd593a687b8817 - filename: injitest/src/main/java/inji/utils/UpdateNetworkSettings.java @@ -365,7 +365,7 @@ fileignoreconfig: - filename: android/app/src/main/AndroidManifest.xml checksum: 8f4bd61770b8bb0a28859ca0f3b4b095aed4e3fb5adef435cb74b9389ff13e09 - filename: ios/Inji.xcodeproj/project.pbxproj - checksum: 5fd66dc8d95628ae831e86caffb46bc0ecf0f42a534ebad76774ba133b8a7907 + checksum: 5bb246fa39bc7a9994b50bc5b1505d5389d0e254a4e30cfb2a57e6a1025e9087 - filename: screens/Settings/ReceivedCardsModal.tsx checksum: 6dee9153a61009b0252d294154c88d5e1b241a517c76e930b391a39d7bc52392 - filename: components/FaceScanner/FaceCompare.tsx @@ -406,4 +406,8 @@ fileignoreconfig: checksum: 2ab5f935ea3d1ec4d109d8614c2246f40e284594288566338f185611470e6928 - filename: components/VC/common/VCUtils.tsx checksum: 221b56a2aeb73f39677cdb7c7528bfe17e49092ce785431d20b2cdfffb96d9cf + - filename: shared/vcRenderer/VcRenderer.ts + checksum: 02f7d58fb149ecd3f10dd0bdfb6e85c4b3ae41d29a40d192056ffec0367b53c6 + - filename: components/VC/Views/VCDetailView.tsx + checksum: 28537e64ff03c0bf9580d02fda145b49181a073c3df3f792674275a62f1e5667 version: "1.0" diff --git a/android/app/build.gradle b/android/app/build.gradle index 9fcf4752..70cc8fa0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -267,6 +267,7 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + implementation("io.mosip:injivcrenderer-aar:0.1.0-SNAPSHOT") implementation("io.mosip:inji-openid4vp-aar:0.5.0-SNAPSHOT"){ changing = true exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on' 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 5eb88d21..cd95030f 100644 --- a/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java +++ b/android/app/src/main/java/io/mosip/residentapp/InjiPackage.java @@ -28,6 +28,7 @@ public class InjiPackage implements ReactPackage { modules.add(new RNDeepLinkIntentModule(reactApplicationContext)); modules.add(new InjiOpenID4VPModule(reactApplicationContext)); modules.add(new RNVCVerifierModule(reactApplicationContext)); + modules.add(new RNInjiVcRendererModule(reactApplicationContext)); return modules; } diff --git a/android/app/src/main/java/io/mosip/residentapp/RNInjiVcRendererModule.java b/android/app/src/main/java/io/mosip/residentapp/RNInjiVcRendererModule.java new file mode 100644 index 00000000..f9012df3 --- /dev/null +++ b/android/app/src/main/java/io/mosip/residentapp/RNInjiVcRendererModule.java @@ -0,0 +1,66 @@ +package io.mosip.residentapp; + +import androidx.annotation.Nullable; + +import com.facebook.react.bridge.Arguments; +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.WritableArray; +import com.facebook.react.bridge.WritableNativeArray; + +import java.util.List; + +import io.mosip.injivcrenderer.InjiVcRenderer; +import io.mosip.injivcrenderer.exceptions.VcRendererExceptions; +import io.mosip.injivcrenderer.constants.CredentialFormat; + +public class RNInjiVcRendererModule extends ReactContextBaseJavaModule { + private static final String MODULE_NAME = "InjiVcRenderer"; + + private InjiVcRenderer injiVcRenderer; + + public RNInjiVcRendererModule(@Nullable ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return MODULE_NAME; + } + @ReactMethod + public void init(String appId) { + injiVcRenderer = new InjiVcRenderer(appId); + } + + @ReactMethod + public void renderVC(String credentialFormat, String wellKnown, String vcJsonString, Promise promise) { + try { + List results = injiVcRenderer.renderVC( + CredentialFormat.Companion.fromValue(credentialFormat), + wellKnown, + vcJsonString + ); + + WritableArray resultArray = new WritableNativeArray(); + for (Object obj : results) { + String svg = obj.toString(); + resultArray.pushString(svg); + } + promise.resolve(resultArray); + } catch (Exception e) { + rejectWithVcRendererExceptions(e, promise); + } + } + + @ReactMethod + private static void rejectWithVcRendererExceptions(Exception e, Promise promise) { + if (e instanceof VcRendererExceptions) { + VcRendererExceptions ex = (VcRendererExceptions) e; + promise.reject(ex.getErrorCode(), ex.getMessage(), ex); + } else { + promise.reject("ERR_UNKNOWN", e.getMessage(), e); + } + } +} diff --git a/components/QrCodeOverlay.tsx b/components/QrCodeOverlay.tsx index 02ad3f1c..d882544d 100644 --- a/components/QrCodeOverlay.tsx +++ b/components/QrCodeOverlay.tsx @@ -77,34 +77,42 @@ export const QrCodeOverlay: React.FC = props => { }, []); const [isQrOverlayVisible, setIsQrOverlayVisible] = useState(false); - const toggleQrOverlay = () => setIsQrOverlayVisible(!isQrOverlayVisible); + const overlayVisible = props.forceVisible ?? isQrOverlayVisible; + + const toggleQrOverlay = () => { + if (props.onClose) props.onClose(); + else setIsQrOverlayVisible(!overlayVisible); + }; + return ( qrString != '' && !qrError && ( - - - - {SvgImage.MagnifierZoom()} - - + {props.showInlineQr !== false && ( + + + + {SvgImage.MagnifierZoom()} + + + )} @@ -161,4 +169,7 @@ export const QrCodeOverlay: React.FC = props => { interface QrCodeOverlayProps { verifiableCredential: VerifiableCredential; meta: VCMetadata; + showInlineQr?: boolean; + forceVisible?: boolean; + onClose?: () => void; } diff --git a/components/VC/Views/VCDetailView.tsx b/components/VC/Views/VCDetailView.tsx index 638e8861..30c8e9ca 100644 --- a/components/VC/Views/VCDetailView.tsx +++ b/components/VC/Views/VCDetailView.tsx @@ -1,8 +1,17 @@ -import React, { useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, {useEffect, useState} from 'react'; +import {useTranslation} from 'react-i18next'; import Icon from 'react-native-vector-icons/FontAwesome'; -import Feather from 'react-native-vector-icons/Feather'; -import { Image, ImageBackground, ImageBackgroundProps, TouchableOpacity, View } from 'react-native'; +import Feather from 'react-native-vector-icons/Feather'; +import { + Dimensions, + Image, + ImageBackground, + ImageBackgroundProps, + TouchableOpacity, + View, +} from 'react-native'; +import {ActivityIndicator} from '../../ui/ActivityIndicator'; + import { Credential, CredentialWrapper, @@ -10,26 +19,28 @@ import { VerifiableCredentialData, WalletBindingResponse, } from '../../../machines/VerifiableCredential/VCMetaMachine/vc'; -import { Button, Column, Row, Text } from '../../ui'; -import { Theme } from '../../ui/styleUtils'; -import { QrCodeOverlay } from '../../QrCodeOverlay'; -import { SvgImage } from '../../ui/svg'; -import { isActivationNeeded } from '../../../shared/openId4VCI/Utils'; +import {Button, Column, Row, Text} from '../../ui'; +import {Theme} from '../../ui/styleUtils'; +import {QrCodeOverlay} from '../../QrCodeOverlay'; +import {SvgImage} from '../../ui/svg'; +import {isActivationNeeded} from '../../../shared/openId4VCI/Utils'; import { BOTTOM_SECTION_FIELDS_WITH_DETAILED_ADDRESS_FIELDS, DETAIL_VIEW_BOTTOM_SECTION_FIELDS, Display, fieldItemIterator, } from '../common/VCUtils'; -import { VCFormat } from '../../../shared/VCFormat'; +import {VCFormat} from '../../../shared/VCFormat'; import testIDProps from '../../../shared/commonUtil'; -import { ShareableInfoModal } from './ShareableInfoModal'; +import {ShareableInfoModal} from './ShareableInfoModal'; +import {SvgCss} from 'react-native-svg/css'; +import {QR_IMAGE_ID} from '../../../shared/constants'; const getProfileImage = (face: any) => { if (face) { return ( - + ); } return <>; @@ -38,12 +49,38 @@ const getProfileImage = (face: any) => { export const VCDetailView: React.FC = ( props: VCItemDetailsProps, ) => { - const { t } = useTranslation('VcDetails'); + const {t} = useTranslation('VcDetails'); const logo = props.verifiableCredentialData.issuerLogo; const face = props.verifiableCredentialData.face; const verifiableCredential = props.credential; const wellknownDisplayProperty = new Display(props.wellknown); + const [svgAspectRatio, setSvgAspectRatio] = useState(null); + + const {width: deviceWidth} = Dimensions.get('window'); + const targetHeight = svgAspectRatio + ? deviceWidth * svgAspectRatio + : deviceWidth * 0.7; + + const svgTemplate = + Array.isArray(props.svgTemplate) && props.svgTemplate.length > 0 + ? props.svgTemplate[0] + : null; + + useEffect(() => { + if (svgTemplate) { + const match = svgTemplate.match(/viewBox="0 0 (\d+) (\d+)"/); + if (match) { + const [, w, h] = match.map(Number); + setSvgAspectRatio(h / w); + } else { + setSvgAspectRatio(null); + } + } else { + setSvgAspectRatio(null); + } + }, [svgTemplate]); + const shouldShowHrLine = verifiableCredential => { let availableFieldNames: string[] = []; if (props.verifiableCredentialData.vcMetadata.format === VCFormat.ldp_vc) { @@ -53,7 +90,10 @@ export const VCDetailView: React.FC = ( } else if ( props.verifiableCredentialData.vcMetadata.format === VCFormat.mso_mdoc ) { - const namespaces = verifiableCredential['issuerSigned']?.['nameSpaces'] ?? verifiableCredential['nameSpaces'] ?? {}; + const namespaces = + verifiableCredential['issuerSigned']?.['nameSpaces'] ?? + verifiableCredential['nameSpaces'] ?? + {}; Object.keys(namespaces).forEach(namespace => { (namespaces[namespace] as Array).forEach(element => { availableFieldNames.push( @@ -61,11 +101,13 @@ export const VCDetailView: React.FC = ( ); }); }); - } - else if ( - props.verifiableCredentialData.vcMetadata.format === VCFormat.vc_sd_jwt || props.verifiableCredentialData.vcMetadata.format === VCFormat.dc_sd_jwt + } else if ( + props.verifiableCredentialData.vcMetadata.format === VCFormat.vc_sd_jwt || + props.verifiableCredentialData.vcMetadata.format === VCFormat.dc_sd_jwt ) { - availableFieldNames = Object.keys(verifiableCredential?.fullResolvedPayload); + availableFieldNames = Object.keys( + verifiableCredential?.fullResolvedPayload, + ); } for (const fieldName of availableFieldNames) { if ( @@ -77,95 +119,146 @@ export const VCDetailView: React.FC = ( return false; }; + const [showQrOverlay, setShowQrOverlay] = useState(false); + const [shareModalVisible, setShareModalVisible] = useState(false); + + if (props.loadingSvg) { + return ( + + + + ); + } + return ( <> - - - - - - {getProfileImage(face)} - - - {logo?.alt_text} + + + + {svgTemplate?.includes(QR_IMAGE_ID) && ( +