[INJIMOB-956]: SVG rendering (#2080)

* [INJIMOB-956]: Android native module integration

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: iOS native module integration and react native layer

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: React native UI design changes

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3499]: Updating android native module integration to adapt latest chagnes in libray api

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3499]: Updated Native module integration

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Update tht package dependency version

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Loading before SVg rendering issue fixed

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: show Qr code button based on fallback image id

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Update Swift package dependency

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-3499]: Update Swift Package dependency to develop of Renderer library

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Update in received screen

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Update format in renderVC call

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Change ordering of the params

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Changes in caching

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

* [INJIMOB-956]: Updating the package dependency files

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>

---------

Signed-off-by: balachandarg-tw <balachandar.g@thoughtworks.com>
This commit is contained in:
balachandarg-tw
2025-09-16 20:58:16 +05:30
committed by GitHub
parent 0b47511283
commit a7d1b9adbf
15 changed files with 593 additions and 140 deletions

View File

@@ -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"

View File

@@ -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'

View File

@@ -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;
}

View File

@@ -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<Object> 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);
}
}
}

View File

@@ -77,34 +77,42 @@ export const QrCodeOverlay: React.FC<QrCodeOverlayProps> = 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 && (
<React.Fragment>
<View testID="qrCodeView" style={Theme.QrCodeStyles.QrView}>
<Pressable
{...testIDProps('qrCodePressable')}
accessible={false}
onPress={toggleQrOverlay}>
<QRCode
{...testIDProps('qrCode')}
size={72}
value={qrString}
backgroundColor={Theme.Colors.QRCodeBackgroundColor}
ecl={DEFAULT_ECL}
onError={onQRError}
/>
<View
testID="magnifierZoom"
style={[Theme.QrCodeStyles.magnifierZoom]}>
{SvgImage.MagnifierZoom()}
</View>
</Pressable>
{props.showInlineQr !== false && (
<Pressable
{...testIDProps('qrCodePressable')}
accessible={false}
onPress={toggleQrOverlay}>
<QRCode
{...testIDProps('qrCode')}
size={72}
value={qrString}
backgroundColor={Theme.Colors.QRCodeBackgroundColor}
ecl={DEFAULT_ECL}
onError={onQRError}
/>
<View
testID="magnifierZoom"
style={[Theme.QrCodeStyles.magnifierZoom]}>
{SvgImage.MagnifierZoom()}
</View>
</Pressable>
)}
</View>
<Overlay
isVisible={isQrOverlayVisible}
isVisible={overlayVisible}
onBackdropPress={toggleQrOverlay}
overlayStyle={{padding: 1, borderRadius: 21}}>
<Column style={Theme.QrCodeStyles.expandedQrCode}>
@@ -161,4 +169,7 @@ export const QrCodeOverlay: React.FC<QrCodeOverlayProps> = props => {
interface QrCodeOverlayProps {
verifiableCredential: VerifiableCredential;
meta: VCMetadata;
showInlineQr?: boolean;
forceVisible?: boolean;
onClose?: () => void;
}

View File

@@ -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 (
<Image source={{ uri: face }} style={Theme.Styles.detailedViewImage} />
<Image source={{uri: face}} style={Theme.Styles.detailedViewImage} />
);
}
return <></>;
@@ -38,12 +49,38 @@ const getProfileImage = (face: any) => {
export const VCDetailView: React.FC<VCItemDetailsProps> = (
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<number | null>(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<VCItemDetailsProps> = (
} 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<Object>).forEach(element => {
availableFieldNames.push(
@@ -61,11 +101,13 @@ export const VCDetailView: React.FC<VCItemDetailsProps> = (
);
});
});
}
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<VCItemDetailsProps> = (
return false;
};
const [showQrOverlay, setShowQrOverlay] = useState(false);
const [shareModalVisible, setShareModalVisible] = useState(false);
if (props.loadingSvg) {
return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<ActivityIndicator />
</View>
);
}
return (
<>
<Column scroll>
<Column fill>
<Column
padding="10 10 3 10"
backgroundColor={Theme.Colors.DetailedViewBackground}>
<ImageBackground
imageStyle={Theme.Styles.vcDetailBg}
resizeMethod="scale"
resizeMode="stretch"
style={[
Theme.Styles.openCardBgContainer,
wellknownDisplayProperty.getBackgroundColor(),
]}
source={
wellknownDisplayProperty.getBackgroundImage(
Theme.OpenCard,
) as ImageBackgroundProps
}>
<Row padding="14 14 0 14" margin="0 0 0 0">
<Column crossAlign="center">
{getProfileImage(face)}
<QrCodeOverlay
verifiableCredential={
props.credentialWrapper as unknown as VerifiableCredential
}
meta={props.verifiableCredentialData.vcMetadata}
/>
<Column
width={80}
height={59}
crossAlign="center"
margin="12 0 0 0">
<Image
{...testIDProps('issuerLogo')}
src={logo?.url}
alt={logo?.alt_text}
style={Theme.Styles.issuerLogo}
resizeMethod="scale"
resizeMode="contain"
{svgTemplate ? (
<Column padding="30 0 0 0">
<Column padding="0 16 0 16">
<SvgCss
xml={svgTemplate}
width="100%"
height={targetHeight}
preserveAspectRatio="xMidYMid meet"
style={{backgroundColor: 'transparent'}}
/>
</Column>
{svgTemplate?.includes(QR_IMAGE_ID) && (
<Button
testID="zoomQrCode"
title="Tap to zoom QR Code"
type="gradient"
size="Large"
onPress={() => setShowQrOverlay(true)}
/>
)}
{showQrOverlay && (
<QrCodeOverlay
verifiableCredential={
props.credentialWrapper as unknown as VerifiableCredential
}
meta={props.verifiableCredentialData.vcMetadata}
showInlineQr={false}
forceVisible={true}
onClose={() => setShowQrOverlay(false)}
/>
)}
</Column>
) : (
<Column fill>
<Column
padding="10 10 3 10"
backgroundColor={Theme.Colors.DetailedViewBackground}>
<ImageBackground
imageStyle={Theme.Styles.vcDetailBg}
resizeMethod="scale"
resizeMode="stretch"
style={[
Theme.Styles.openCardBgContainer,
wellknownDisplayProperty.getBackgroundColor(),
]}
source={
wellknownDisplayProperty.getBackgroundImage(
Theme.OpenCard,
) as ImageBackgroundProps
}>
<Row padding="14 14 0 14" margin="0 0 0 0">
<Column crossAlign="center">
{getProfileImage(face)}
<QrCodeOverlay
verifiableCredential={
props.credentialWrapper as unknown as VerifiableCredential
}
meta={props.verifiableCredentialData.vcMetadata}
showInlineQr={true}
/>
<Column
width={80}
height={59}
crossAlign="center"
margin="12 0 0 0">
<Image
{...testIDProps('issuerLogo')}
src={logo?.url}
alt={logo?.alt_text}
style={Theme.Styles.issuerLogo}
resizeMethod="scale"
resizeMode="contain"
/>
</Column>
</Column>
</Column>
<Column
align="space-evenly"
margin={'0 0 0 24'}
style={{ flex: 1 }}>
{fieldItemIterator(
props.fields,
props.wellknownFieldsFlag,
verifiableCredential,
props.wellknown,
wellknownDisplayProperty,
false,
props,
)}
</Column>
</Row>
<>
<View
style={[
Theme.Styles.hrLine,
{
borderBottomColor: wellknownDisplayProperty.getTextColor(
Theme.Styles.hrLine.borderBottomColor,
),
},
]}></View>
<Column padding="0 14 14 14">
{shouldShowHrLine(verifiableCredential) &&
fieldItemIterator(
DETAIL_VIEW_BOTTOM_SECTION_FIELDS,
true,
<Column
align="space-evenly"
margin={'0 0 0 24'}
style={{flex: 1}}>
{fieldItemIterator(
props.fields,
props.wellknownFieldsFlag,
verifiableCredential,
props.wellknown,
wellknownDisplayProperty,
true,
false,
props,
)}
</Column>
{(props.credential.disclosedKeys != null) && (<DisclosureInfoNote />)}
</>
</ImageBackground>
</Column>
</Row>
<>
<View
style={[
Theme.Styles.hrLine,
{
borderBottomColor:
wellknownDisplayProperty.getTextColor(
Theme.Styles.hrLine.borderBottomColor,
),
},
]}></View>
<Column padding="0 14 14 14">
{shouldShowHrLine(verifiableCredential) &&
fieldItemIterator(
DETAIL_VIEW_BOTTOM_SECTION_FIELDS,
true,
verifiableCredential,
props.wellknown,
wellknownDisplayProperty,
true,
props,
)}
</Column>
{props.credential.disclosedKeys != null && (
<DisclosureInfoNote />
)}
</>
</ImageBackground>
</Column>
</Column>
</Column>
)}
</Column>
{props.vcHasImage &&
{!svgTemplate &&
props.vcHasImage &&
!props.verifiableCredentialData?.vcMetadata.isExpired && (
<View
style={{
@@ -245,9 +338,8 @@ export const VCDetailView: React.FC<VCItemDetailsProps> = (
))}
</View>
)}
{
(props.credential.disclosedKeys != null) && (<View
{props.credential.disclosedKeys != null && (
<View
style={{
padding: 16,
backgroundColor: Theme.Colors.DetailedViewBackground,
@@ -258,8 +350,8 @@ export const VCDetailView: React.FC<VCItemDetailsProps> = (
<TouchableOpacity
onPress={() => setShareModalVisible(true)}
testID="viewShareableInfoLink"
style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Feather name="eye" size={20} color={"#007AFF"} />
style={{flexDirection: 'row', alignItems: 'center', gap: 8}}>
<Feather name="eye" size={20} color={'#007AFF'} />
<Text
style={{
color: '#007AFF',
@@ -269,34 +361,30 @@ export const VCDetailView: React.FC<VCItemDetailsProps> = (
{t('View Shareable Information')}
</Text>
</TouchableOpacity>
</View>)}
</View>
)}
<ShareableInfoModal
isVisible={shareModalVisible}
onDismiss={() => setShareModalVisible(false)}
disclosedPaths={Array.from(props.credential.disclosedKeys ?? {}) || []}
/>
</>
);
};
export const DisclosureInfoNote = () => {
const { t } = useTranslation('VcDetails');
const {t} = useTranslation('VcDetails');
return (
<View
style={Theme.DisclosureInfo.view}>
<View style={Theme.DisclosureInfo.view}>
<Row align="flex-start">
<Icon
name="share-square-o"
size={18}
color={Theme.Colors.DetailsLabel}
style={{ marginTop: 2, marginRight: 8 }}
style={{marginTop: 2, marginRight: 8}}
/>
<Text
style={Theme.DisclosureInfo.text}>
{t('disclosureInfoNote')}
</Text>
<Text style={Theme.DisclosureInfo.text}>{t('disclosureInfoNote')}</Text>
</Row>
</View>
);
@@ -313,4 +401,7 @@ export interface VCItemDetailsProps {
onBinding?: () => void;
activeTab?: Number;
vcHasImage: boolean;
svgTemplate?: string[] | null;
svgRendererError?: string | null;
loadingSvg?: string | null;
}

View File

@@ -36,8 +36,11 @@
9CCCA19E2CF87A8400D5A461 /* securekeystore in Frameworks */ = {isa = PBXBuildFile; productRef = 9CCCA19D2CF87A8400D5A461 /* securekeystore */; };
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
C339223B2E79A536004A01EC /* InjiVcRenderer in Frameworks */ = {isa = PBXBuildFile; productRef = C339223A2E79A536004A01EC /* InjiVcRenderer */; };
C3F18B1A2E320C85007DBE73 /* OpenID4VP in Frameworks */ = {isa = PBXBuildFile; productRef = C3F18B192E320C85007DBE73 /* OpenID4VP */; };
C3F18B1D2E320C9A007DBE73 /* VCIClient in Frameworks */ = {isa = PBXBuildFile; productRef = C3F18B1C2E320C9A007DBE73 /* VCIClient */; };
C3F6A9DD2E661896006C9904 /* RNInjiVcRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3F6A9DC2E66188D006C9904 /* RNInjiVcRenderer.swift */; };
C3F6A9DF2E661903006C9904 /* RNInjiVcRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = C3F6A9DE2E6618F6006C9904 /* RNInjiVcRenderer.m */; };
E86208152C0335C5007C3E24 /* RNVCIClientModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86208142C0335C5007C3E24 /* RNVCIClientModule.swift */; };
E86208172C0335EC007C3E24 /* RNVCIClientModule.m in Sources */ = {isa = PBXBuildFile; fileRef = E86208162C0335EC007C3E24 /* RNVCIClientModule.m */; };
FD8D20B92BBAACDF009AD01C /* Fontisto.ttf in Resources */ = {isa = PBXBuildFile; fileRef = FD8D20A62BBAACDF009AD01C /* Fontisto.ttf */; };
@@ -92,6 +95,8 @@
9C7CDF422C7CC13500243A9A /* RNSecureKeystoreModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNSecureKeystoreModule.m; sourceTree = "<group>"; };
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Inji/SplashScreen.storyboard; sourceTree = "<group>"; };
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
C3F6A9DC2E66188D006C9904 /* RNInjiVcRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNInjiVcRenderer.swift; sourceTree = "<group>"; };
C3F6A9DE2E6618F6006C9904 /* RNInjiVcRenderer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNInjiVcRenderer.m; sourceTree = "<group>"; };
D98B96A488E54CBDB286B26F /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Inji/noop-file.swift"; sourceTree = "<group>"; };
E86208142C0335C5007C3E24 /* RNVCIClientModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RNVCIClientModule.swift; sourceTree = "<group>"; };
E86208162C0335EC007C3E24 /* RNVCIClientModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNVCIClientModule.m; sourceTree = "<group>"; };
@@ -124,6 +129,7 @@
buildActionMask = 2147483647;
files = (
C3F18B1D2E320C9A007DBE73 /* VCIClient in Frameworks */,
C339223B2E79A536004A01EC /* InjiVcRenderer in Frameworks */,
9C4850432C3E5873002ECBD5 /* ios-tuvali-library in Frameworks */,
9CAE74EE2E2E38F800C2532C /* pixelpass in Frameworks */,
9CCCA19E2CF87A8400D5A461 /* securekeystore in Frameworks */,
@@ -138,6 +144,8 @@
13B07FAE1A68108700A75B9A /* Inji */ = {
isa = PBXGroup;
children = (
C3F6A9DE2E6618F6006C9904 /* RNInjiVcRenderer.m */,
C3F6A9DC2E66188D006C9904 /* RNInjiVcRenderer.swift */,
BB2F792B24A3F905000567C9 /* Supporting */,
008F07F21AC5B25A0029DE68 /* main.jsbundle */,
13B07FAF1A68108700A75B9A /* AppDelegate.h */,
@@ -317,6 +325,7 @@
9CAE74ED2E2E38F800C2532C /* pixelpass */,
C3F18B192E320C85007DBE73 /* OpenID4VP */,
C3F18B1C2E320C9A007DBE73 /* VCIClient */,
C339223A2E79A536004A01EC /* InjiVcRenderer */,
);
productName = Inji;
productReference = 13B07F961A680F5B00A75B9A /* Inji.app */;
@@ -350,6 +359,7 @@
9CAE74EC2E2E38F800C2532C /* XCRemoteSwiftPackageReference "pixelpass-ios-swift" */,
C3F18B182E320C85007DBE73 /* XCRemoteSwiftPackageReference "inji-openid4vp-ios-swift" */,
C3F18B1B2E320C9A007DBE73 /* XCRemoteSwiftPackageReference "inji-vci-client-ios-swift" */,
C33922392E79A536004A01EC /* XCRemoteSwiftPackageReference "inji-vc-renderer-ios-swift" */,
);
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
@@ -564,6 +574,7 @@
1EED69FD2DA914D00042EAFC /* RNDeepLinkIntentModule.m in Sources */,
1E6875ED2CA5550F0086D870 /* RNOpenID4VPModule.swift in Sources */,
1E55C20B2DB120C2009DF38B /* RNDeepLinkIntentModule.swift in Sources */,
C3F6A9DD2E661896006C9904 /* RNInjiVcRenderer.swift in Sources */,
9C48504F2C3E59B5002ECBD5 /* RNVersionModule.swift in Sources */,
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
9C0E86BB2BEE36C300E9F9F6 /* RNPixelpassModule.m in Sources */,
@@ -583,6 +594,7 @@
9C48504D2C3E59B5002ECBD5 /* RNWalletModule.m in Sources */,
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
9C4850512C3E59B5002ECBD5 /* RNVersionModule.m in Sources */,
C3F6A9DF2E661903006C9904 /* RNInjiVcRenderer.m in Sources */,
73295844242A4AD3AA52D0BE /* noop-file.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -849,6 +861,14 @@
version = 0.3.0;
};
};
C33922392E79A536004A01EC /* XCRemoteSwiftPackageReference "inji-vc-renderer-ios-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mosip/inji-vc-renderer-ios-swift.git";
requirement = {
branch = develop;
kind = branch;
};
};
C3F18B182E320C85007DBE73 /* XCRemoteSwiftPackageReference "inji-openid4vp-ios-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/mosip/inji-openid4vp-ios-swift.git";
@@ -883,6 +903,11 @@
package = 9CCCA19C2CF87A8400D5A461 /* XCRemoteSwiftPackageReference "secure-keystore-ios-swift" */;
productName = securekeystore;
};
C339223A2E79A536004A01EC /* InjiVcRenderer */ = {
isa = XCSwiftPackageProductDependency;
package = C33922392E79A536004A01EC /* XCRemoteSwiftPackageReference "inji-vc-renderer-ios-swift" */;
productName = InjiVcRenderer;
};
C3F18B192E320C85007DBE73 /* OpenID4VP */ = {
isa = XCSwiftPackageProductDependency;
package = C3F18B182E320C85007DBE73 /* XCRemoteSwiftPackageReference "inji-openid4vp-ios-swift" */;

View File

@@ -1,5 +1,5 @@
{
"originHash" : "327f856a9787c0e4bf4517bc1e50ff1e45a4aee6adcc4233b1e042d022a881f0",
"originHash" : "c97fbee22fce758b0bea2f875d3805ef3530cfb7d60c2832b0b36cf3f72da606",
"pins" : [
{
"identity" : "alamofire",
@@ -73,6 +73,15 @@
"revision" : "ca4935478360f45dd20444eac5d6bae9affc6d36"
}
},
{
"identity" : "inji-vc-renderer-ios-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/mosip/inji-vc-renderer-ios-swift.git",
"state" : {
"branch" : "develop",
"revision" : "58237eae6caf198d725ecfcd397b4d6f8f2d87bd"
}
},
{
"identity" : "inji-vci-client-ios-swift",
"kind" : "remoteSourceControl",

16
ios/RNInjiVcRenderer.m Normal file
View File

@@ -0,0 +1,16 @@
#import <Foundation/Foundation.h>
#import "React/RCTBridgeModule.h"
@interface RCT_EXTERN_MODULE(InjiVcRenderer, NSObject)
RCT_EXTERN_METHOD(init:(NSString *)traceabilityId)
RCT_EXTERN_METHOD(renderVC:(NSString *)credentialFormat
wellKnown:(NSString *)wellKnown
vcJsonString:(NSString *)vcJsonString
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
@end

View File

@@ -0,0 +1,57 @@
import Foundation
import React
import InjiVcRenderer
@objc(InjiVcRenderer)
class RNInjiVcRenderer: NSObject, RCTBridgeModule {
private var vcRenderer: InjiVcRenderer?
@objc static func requiresMainQueueSetup() -> Bool {
return false
}
static func moduleName() -> String {
return "InjiVcRenderer"
}
@objc
func `init`(_ traceabilityId: String) {
vcRenderer = InjiVcRenderer(traceabilityId: traceabilityId)
}
@objc(renderVC:wellKnown:vcJsonString:resolver:rejecter:)
func renderVC(
credentialFormat: String,
wellKnown: String?,
vcJsonString: String,
resolve: @escaping RCTPromiseResolveBlock,
reject: @escaping RCTPromiseRejectBlock
) {
guard let renderer = self.vcRenderer else {
reject(nil, "InjiVcRenderer not initialized", nil)
return
}
do {
let format = CredentialFormat.fromValue(credentialFormat)
let result = try renderer.renderVC(
credentialFormat: format,
wellKnownJson: wellKnown,
vcJsonString: vcJsonString
)
resolve(result)
} catch {
rejectWithVcRendererError(error, reject: reject)
}
}
func rejectWithVcRendererError(_ error: Error, reject: RCTPromiseRejectBlock) {
if let vcRendererError = error as? VcRendererException {
reject(vcRendererError.errorCode, vcRendererError.message, vcRendererError)
} else {
let nsError = NSError(domain: error.localizedDescription, code: 0)
reject("ERR_UNKNOWN", nsError.localizedDescription, nsError)
}
}
}

View File

@@ -33,6 +33,7 @@ import {
} from '../../components/BannerNotification';
import {VCProcessor} from '../../components/VC/common/VCProcessor';
import {HelpIcon} from '../../components/ui/HelpIcon';
import VcRenderer from '../../shared/vcRenderer/VcRenderer';
export const ViewVcModal: React.FC<ViewVcModalProps> = props => {
const {t} = useTranslation('ViewVcModal');
@@ -40,6 +41,10 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = props => {
const profileImage = controller.verifiableCredentialData.face;
const verificationStatus = controller.verificationStatus;
const [verifiableCredential, setVerifiableCredential] = useState(null);
const [svgTemplate, setSvgTemplate] = useState<string[] | null>(null);
const [svgRendererError, setSvgRendererError] = useState<string[] | null>(
null,
);
useEffect(() => {
async function processVC() {
@@ -72,6 +77,37 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = props => {
const [wellknownFieldsFlag, setWellknownFieldsFlag] = useState(false);
const verifiableCredentialData = controller.verifiableCredentialData;
const [loadingSvg, setLoadingSvg] = useState<boolean>(true);
useEffect(() => {
const fetchSvg = async () => {
try {
setLoadingSvg(true);
const vcJsonString = JSON.stringify(controller.credential.credential);
const result = await VcRenderer.getInstance().renderVC(
controller.verifiableCredentialData.format,
wellknown ?? null,
vcJsonString,
);
setSvgTemplate(result);
setSvgRendererError(null);
} catch (err: any) {
setSvgTemplate(null);
setSvgRendererError(err.errorCode ?? 'Unknown error');
} finally {
setLoadingSvg(false);
}
};
if (controller.credential?.credential['renderMethod']) {
requestAnimationFrame(fetchSvg);
} else {
setLoadingSvg(false);
}
}, [controller.credential?.credential, wellknown]);
useEffect(() => {
getDetailedViewFields(
verifiableCredentialData.vcMetadata.issuerHost as string,
@@ -79,13 +115,15 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = props => {
DETAIL_VIEW_DEFAULT_FIELDS,
verifiableCredentialData.vcMetadata.format,
verifiableCredentialData.vcMetadata.issuerHost,
).then(response => {
setWellknown(response.matchingCredentialIssuerMetadata);
setFields(response.fields);
setWellknownFieldsFlag(response.wellknownFieldsFlag);
}).catch(error => {
console.error('Error fetching well-known fields:', error);
});
)
.then(response => {
setWellknown(response.matchingCredentialIssuerMetadata);
setFields(response.fields);
setWellknownFieldsFlag(response.wellknownFieldsFlag);
})
.catch(error => {
console.error('Error fetching well-known fields:', error);
});
}, [verifiableCredentialData?.wellKnown]);
const headerRight = flow => {
@@ -164,6 +202,9 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = props => {
walletBindingResponse={controller.walletBindingResponse}
activeTab={props.activeTab}
vcHasImage={profileImage !== undefined}
svgTemplate={svgTemplate}
svgRendererError={svgRendererError}
loadingSvg={loadingSvg}
/>
)}

View File

@@ -12,6 +12,7 @@ import {SvgImage} from '../../components/ui/svg';
import {DETAIL_VIEW_DEFAULT_FIELDS} from '../../components/VC/common/VCUtils';
import {getDetailedViewFields} from '../../shared/openId4VCI/Utils';
import {VCProcessor} from '../../components/VC/common/VCProcessor';
import VcRenderer from '../../shared/vcRenderer/VcRenderer';
export const ReceiveVcScreen: React.FC = () => {
const {t} = useTranslation('ReceiveVcScreen');
@@ -26,6 +27,12 @@ export const ReceiveVcScreen: React.FC = () => {
const [credential, setCredential] = useState(null);
const [svgTemplate, setSvgTemplate] = useState<string[] | null>(null);
const [svgRendererError, setSvgRendererError] = useState<string[] | null>(
null,
);
const [loadingSvg, setLoadingSvg] = useState<boolean>(true);
useEffect(() => {
async function processVC() {
if (controller.credential) {
@@ -40,6 +47,35 @@ export const ReceiveVcScreen: React.FC = () => {
processVC();
}, [controller.credential]);
useEffect(() => {
const fetchSvg = async () => {
try {
setLoadingSvg(true);
const vcJsonString = JSON.stringify(controller.credential.credential);
const result = await VcRenderer.getInstance().renderVC(
verifiableCredentialData.vcMetadata.format,
wellknown ?? null,
vcJsonString,
);
setSvgTemplate(result);
setSvgRendererError(null);
} catch (err: any) {
setSvgTemplate(null);
setSvgRendererError(err.errorCode ?? 'Unknown error');
} finally {
setLoadingSvg(false);
}
};
if (controller.credential?.credential['renderMethod']) {
requestAnimationFrame(fetchSvg);
} else {
setLoadingSvg(false);
}
}, [controller.credential?.credential, wellknown]);
useEffect(() => {
getDetailedViewFields(
verifiableCredentialData.vcMetadata.issuerHost,
@@ -78,6 +114,9 @@ export const ReceiveVcScreen: React.FC = () => {
isBindingPending={false}
activeTab={1}
vcHasImage={profileImage !== undefined}
svgTemplate={svgTemplate}
svgRendererError={svgRendererError}
loadingSvg={loadingSvg}
/>
</Column>
<Column padding="0 24" margin="32 0 0 0">

View File

@@ -1,6 +1,6 @@
import {sha256} from '@noble/hashes/sha256';
import {VCMetadata} from './VCMetadata';
import {NETWORK_REQUEST_FAILED} from './constants';
import {CACHE_TTL, NETWORK_REQUEST_FAILED} from './constants';
import {groupBy} from './javascript';
import {Issuers} from './openId4VCI/Utils';
import {v4 as uuid} from 'uuid';
@@ -135,3 +135,7 @@ export const createCacheObject = (response: any) => {
cachedTime: currentTime,
};
};
export const isCacheExpired = (timestamp: number) => {
return Date.now() - timestamp >= CACHE_TTL;
};

View File

@@ -201,3 +201,5 @@ export const OVP_ERROR_CODE = {
NO_MATCHING_VCS: 'invalid_transaction_data',
DECLINED: 'access_denied',
};
export const QR_IMAGE_ID = 'qrCodeImage';

View File

@@ -0,0 +1,86 @@
import {NativeModules} from 'react-native';
import {MMKVLoader} from 'react-native-mmkv-storage';
import {CACHE_TTL} from '../constants';
import {__AppId} from '../GlobalVariables';
import {isCacheExpired} from '../Utils';
const MMKV = new MMKVLoader().initialize();
const CACHE_KEY_PREFIX = 'vc_renderer_svg_';
type CachedSvg = {
data: string[];
timestamp: number;
};
class VcRenderer {
private static instance: VcRenderer;
private InjiVcRenderer = NativeModules.InjiVcRenderer;
private constructor() {
this.InjiVcRenderer.init(__AppId.getValue());
}
static getInstance(): VcRenderer {
if (!VcRenderer.instance) {
VcRenderer.instance = new VcRenderer();
}
return VcRenderer.instance;
}
private createCacheKey(vcId: string) {
return `${CACHE_KEY_PREFIX}${vcId}`;
}
async renderVC(
credentialFormat: string,
wellKnown: string,
vcJson: string,
): Promise<string[]> {
const vc = JSON.parse(vcJson);
const vcId = vc.id ?? JSON.stringify(vc);
const cacheKey = this.createCacheKey(vcId);
const cachedRaw = await MMKV.getItem(cacheKey);
if (cachedRaw && typeof cachedRaw === 'string') {
try {
const cached: CachedSvg = JSON.parse(cachedRaw);
if (!isCacheExpired(cached.timestamp)) {
return cached.data;
} else {
await this.clearCache(vcId);
}
} catch (e) {
console.warn('::::failed to parse cached SVG, ignoring::::', e);
await this.clearCache(vcId);
}
}
try {
const result: string[] = await this.InjiVcRenderer.renderVC(
credentialFormat,
wellKnown ? JSON.stringify(wellKnown) : null,
vcJson,
);
if (result && result.length > 0) {
const payload: CachedSvg = {
data: result,
timestamp: Date.now(),
};
await MMKV.setItem(cacheKey, JSON.stringify(payload));
}
return result;
} catch (rendererError) {
await this.clearCache(vcId);
throw rendererError;
}
}
async clearCache(vcId: string) {
const cacheKey = this.createCacheKey(vcId);
await MMKV.removeItem(cacheKey);
}
}
export default VcRenderer;