mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-09 05:27:57 -05:00
[INJIMOB-3532] add sd jwt vp support (#2082)
* [INJIMOB-3513] add sd jwt vp support Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com> * [INJIMOB-3513] add bridge logic and sd-jwt signing for ovp Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com> * [INJIMOB-3532] add: support of OVP share in iOS Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com> * [INJIMOB-3532] add sd-jwt ovp ui Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com> * [INJIMOB-3532] refactor: optimize wallet_metadata creation logic Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com> * [INJIMOB-3532] refactor: fixed alignement issues and crash bug Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com> --------- Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com> Signed-off-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com> Co-authored-by: KiruthikaJeyashankar <kiruthikavjshankar@gmail.com>
This commit is contained in:
92
components/TrustModal.tsx
Normal file
92
components/TrustModal.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Modal, View, Text, Image, ScrollView } from 'react-native';
|
||||
import { Button } from './ui';
|
||||
import { Theme } from './ui/styleUtils';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const TrustModal = ({
|
||||
isVisible,
|
||||
logo,
|
||||
name,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
flowType = 'issuer',
|
||||
}: {
|
||||
isVisible: boolean;
|
||||
logo: any;
|
||||
name: string;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
flowType?: 'issuer' | 'verifier';
|
||||
}) => {
|
||||
const { t } = useTranslation('trustScreen');
|
||||
return (
|
||||
<Modal transparent={true} visible={isVisible} animationType="fade">
|
||||
<View style={Theme.TrustIssuerScreenStyle.modalOverlay}>
|
||||
<View style={Theme.TrustIssuerScreenStyle.modalContainer}>
|
||||
{(logo || name) && (
|
||||
<View style={Theme.TrustIssuerScreenStyle.issuerHeader}>
|
||||
{logo && (
|
||||
<Image
|
||||
source={{ uri: logo }}
|
||||
style={Theme.TrustIssuerScreenStyle.issuerLogo}
|
||||
/>
|
||||
)}
|
||||
{name && (
|
||||
<Text style={Theme.TrustIssuerScreenStyle.issuerName}>
|
||||
{name}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
<ScrollView
|
||||
style={{ flex: 1, width: '100%' }}
|
||||
contentContainerStyle={{ alignItems: 'center', paddingBottom: 10 }}
|
||||
showsVerticalScrollIndicator={true}>
|
||||
<Text style={Theme.TrustIssuerScreenStyle.description}>
|
||||
{t(flowType == 'issuer' ? 'description' : 'verifierDescription')}
|
||||
</Text>
|
||||
|
||||
<View style={Theme.TrustIssuerScreenStyle.infoContainer}>
|
||||
{t(flowType == 'issuer' ? 'infoPoints' : 'verifierInfoPoints', { returnObjects: true }).map((point, index) => (
|
||||
<View key={index} style={Theme.TrustIssuerScreenStyle.infoItem}>
|
||||
<Text style={Theme.TrustIssuerScreenStyle.info}>•</Text>
|
||||
<Text style={Theme.TrustIssuerScreenStyle.infoText}>
|
||||
{point}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={{ width: '100%', paddingTop: 10, paddingBottom: 5 }}>
|
||||
<Button
|
||||
styles={{
|
||||
marginBottom: 3,
|
||||
minHeight: 50,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
type="gradient"
|
||||
title={t(flowType == 'issuer' ? 'confirm' : 'verifierConfirm')}
|
||||
onPress={onConfirm}
|
||||
/>
|
||||
<Button
|
||||
styles={{
|
||||
marginBottom: -10,
|
||||
paddingBottom: 20,
|
||||
minHeight: 60,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
maxWidth: '100%',
|
||||
}}
|
||||
type="clear"
|
||||
title={t('cancel')}
|
||||
onPress={onCancel}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -27,6 +27,7 @@ export const VCCardView: React.FC<VCItemProps> = ({
|
||||
flow,
|
||||
isInitialLaunch = false,
|
||||
isTopCard = false,
|
||||
onDisclosuresChange,
|
||||
}) => {
|
||||
const controller = useVcItemController(vcMetadata);
|
||||
const {t} = useTranslation();
|
||||
@@ -56,17 +57,16 @@ export const VCCardView: React.FC<VCItemProps> = ({
|
||||
setVc(processedData);
|
||||
}
|
||||
}
|
||||
|
||||
loadVc();
|
||||
}, [isDownloading, controller.credential]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!verifiableCredentialData || !verifiableCredentialData.vcMetadata) return;
|
||||
const {
|
||||
issuer,
|
||||
credentialConfigurationId,
|
||||
vcMetadata: { format },
|
||||
} = verifiableCredentialData;
|
||||
|
||||
if (vcMetadata.issuerHost) {
|
||||
getCredentialIssuersWellKnownConfig(
|
||||
vcMetadata.issuerHost,
|
||||
@@ -76,13 +76,13 @@ export const VCCardView: React.FC<VCItemProps> = ({
|
||||
vcMetadata.issuerHost,
|
||||
)
|
||||
.then(response => {
|
||||
if(response && response.matchingCredentialIssuerMetadata) {
|
||||
setWellknown(response.matchingCredentialIssuerMetadata);
|
||||
if (response && response.matchingCredentialIssuerMetadata) {
|
||||
setWellknown(response.matchingCredentialIssuerMetadata);
|
||||
}
|
||||
setFields(response.fields);
|
||||
})
|
||||
.catch(error => {
|
||||
setWellknown({"fallback":"true"});
|
||||
setWellknown({fallback: 'true'});
|
||||
console.error(
|
||||
'Error occurred while fetching wellknown for viewing VC ',
|
||||
error,
|
||||
@@ -94,6 +94,7 @@ export const VCCardView: React.FC<VCItemProps> = ({
|
||||
if (!isVCLoaded(controller.credential) || !wellknown || !vc) {
|
||||
return <VCCardSkeleton />;
|
||||
}
|
||||
|
||||
const CardViewContent = () => (
|
||||
<VCCardViewContent
|
||||
vcMetadata={vcMetadata}
|
||||
@@ -111,6 +112,7 @@ export const VCCardView: React.FC<VCItemProps> = ({
|
||||
DISMISS={controller.DISMISS}
|
||||
KEBAB_POPUP={controller.KEBAB_POPUP}
|
||||
isInitialLaunch={isInitialLaunch}
|
||||
onDisclosuresChange={onDisclosuresChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -159,4 +161,5 @@ export interface VCItemProps {
|
||||
flow?: string;
|
||||
isInitialLaunch?: boolean;
|
||||
isTopCard?: boolean;
|
||||
}
|
||||
onDisclosuresChange?: (paths: string[]) => void;
|
||||
}
|
||||
@@ -1,29 +1,184 @@
|
||||
import React from 'react';
|
||||
import {ImageBackground, Pressable, View, Image, ImageBackgroundProps} from 'react-native';
|
||||
import {VCMetadata} from '../../../shared/VCMetadata';
|
||||
import {KebabPopUp} from '../../KebabPopUp';
|
||||
import {Credential} from '../../../machines/VerifiableCredential/VCMetaMachine/vc';
|
||||
import {Column, Row} from '../../ui';
|
||||
import {Theme} from '../../ui/styleUtils';
|
||||
import {CheckBox, Icon} from 'react-native-elements';
|
||||
import {SvgImage} from '../../ui/svg';
|
||||
import {VcItemContainerProfileImage} from '../../VcItemContainerProfileImage';
|
||||
import {isVCLoaded, getCredentialType, Display} from '../common/VCUtils';
|
||||
import {VCItemFieldValue} from '../common/VCItemField';
|
||||
import {WalletBinding} from '../../../screens/Home/MyVcs/WalletBinding';
|
||||
import {VCVerification} from '../../VCVerification';
|
||||
import {isActivationNeeded} from '../../../shared/openId4VCI/Utils';
|
||||
import {VCItemContainerFlowType} from '../../../shared/Utils';
|
||||
import {RemoveVcWarningOverlay} from '../../../screens/Home/MyVcs/RemoveVcWarningOverlay';
|
||||
import {HistoryTab} from '../../../screens/Home/MyVcs/HistoryTab';
|
||||
import {useCopilot} from 'react-native-copilot';
|
||||
import {useTranslation} from 'react-i18next';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ImageBackground, Pressable, View, Image, ImageBackgroundProps } from 'react-native';
|
||||
import { VCMetadata } from '../../../shared/VCMetadata';
|
||||
import { KebabPopUp } from '../../KebabPopUp';
|
||||
import { Credential } from '../../../machines/VerifiableCredential/VCMetaMachine/vc';
|
||||
import { Column, Row, Text } from '../../ui';
|
||||
import { Theme } from '../../ui/styleUtils';
|
||||
import { CheckBox, Icon } from 'react-native-elements';
|
||||
import { SvgImage } from '../../ui/svg';
|
||||
import { VcItemContainerProfileImage } from '../../VcItemContainerProfileImage';
|
||||
import { isVCLoaded, getCredentialType, Display, formatKeyLabel } from '../common/VCUtils';
|
||||
import { VCItemFieldValue } from '../common/VCItemField';
|
||||
import { WalletBinding } from '../../../screens/Home/MyVcs/WalletBinding';
|
||||
import { VCVerification } from '../../VCVerification';
|
||||
import { isActivationNeeded } from '../../../shared/openId4VCI/Utils';
|
||||
import { VCItemContainerFlowType } from '../../../shared/Utils';
|
||||
import { RemoveVcWarningOverlay } from '../../../screens/Home/MyVcs/RemoveVcWarningOverlay';
|
||||
import { HistoryTab } from '../../../screens/Home/MyVcs/HistoryTab';
|
||||
import { useCopilot } from 'react-native-copilot';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import testIDProps from '../../../shared/commonUtil';
|
||||
|
||||
export const VCCardViewContent: React.FC<VCItemContentProps> = ({
|
||||
isPinned = false,
|
||||
credential,
|
||||
verifiableCredentialData,
|
||||
wellknown,
|
||||
selectable,
|
||||
selected,
|
||||
service,
|
||||
onPress,
|
||||
flow,
|
||||
walletBindingResponse,
|
||||
KEBAB_POPUP,
|
||||
DISMISS,
|
||||
isKebabPopUp,
|
||||
vcMetadata,
|
||||
isInitialLaunch,
|
||||
onDisclosuresChange,
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [selectedFields, setSelectedFields] = useState<Record<string, boolean>>({});
|
||||
useEffect(() => {
|
||||
if (flow === VCItemContainerFlowType.VP_SHARE) {
|
||||
setIsExpanded(selected);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const toggleExpand = () => {
|
||||
if (flow === VCItemContainerFlowType.VP_SHARE) {
|
||||
setIsExpanded(prev => !prev);
|
||||
}
|
||||
};
|
||||
const [expandedNodes, setExpandedNodes] = useState<Record<string, boolean>>({});
|
||||
|
||||
const areAllSelected = (): boolean => {
|
||||
return credential.disclosedKeys.every(key => selectedFields[key]);
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
const updated: Record<string, boolean> = {};
|
||||
|
||||
if (areAllSelected()) {
|
||||
|
||||
credential.disclosedKeys.forEach(key => {
|
||||
updated[key] = false;
|
||||
});
|
||||
} else {
|
||||
|
||||
credential.disclosedKeys.forEach(key => {
|
||||
updated[key] = true;
|
||||
});
|
||||
}
|
||||
|
||||
setSelectedFields(updated);
|
||||
const selectedPaths = Object.keys(updated).filter(k => updated[k]);
|
||||
onDisclosuresChange?.(selectedPaths);
|
||||
};
|
||||
|
||||
|
||||
const DisclosureNode: React.FC<{
|
||||
name: string;
|
||||
node: any;
|
||||
fullPath: string;
|
||||
expandedNodes: Record<string, boolean>;
|
||||
setExpandedNodes: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
||||
}> = ({ name, node, fullPath, expandedNodes, setExpandedNodes }) => {
|
||||
const isExpanded = expandedNodes[fullPath] || false;
|
||||
|
||||
const toggleExpand = () => {
|
||||
setExpandedNodes(prev => ({
|
||||
...prev,
|
||||
[fullPath]: !prev[fullPath],
|
||||
}));
|
||||
};
|
||||
|
||||
const isChecked = selectedFields[fullPath] || false;
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Row crossAlign="center" style={{ justifyContent: "space-between", marginBottom: -10 }}>
|
||||
<Row crossAlign="center">
|
||||
{node.__self && (
|
||||
<CheckBox
|
||||
size={22}
|
||||
checked={isChecked}
|
||||
checkedIcon={SvgImage.selectedCheckBox()}
|
||||
uncheckedIcon={
|
||||
<Icon
|
||||
name="check-box-outline-blank"
|
||||
color={Theme.Colors.uncheckedIcon}
|
||||
size={22}
|
||||
/>
|
||||
}
|
||||
onPress={() => handleFieldToggle(fullPath)}
|
||||
/>
|
||||
)}
|
||||
<Text weight="semibold" color={wellknownDisplayProperty.getTextColor(Theme.Colors.plainText)} style={{ marginLeft: 8 }}>
|
||||
{formatKeyLabel(name)}
|
||||
</Text>
|
||||
</Row>
|
||||
|
||||
{/* Right side: expand/collapse icon */}
|
||||
{Object.keys(node.children).length > 0 && (
|
||||
<Pressable onPress={toggleExpand} style={{ marginLeft: 12 }}>
|
||||
<Icon
|
||||
name={isExpanded ? "expand-less" : "expand-more"}
|
||||
color={Theme.Colors.Icon}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</Row>
|
||||
|
||||
{isExpanded &&
|
||||
Object.entries(node.children).map(([childName, childNode]) => (
|
||||
<Column key={childName} margin="0 0 0 15">
|
||||
<DisclosureNode
|
||||
name={childName}
|
||||
node={childNode}
|
||||
fullPath={`${fullPath}.${childName}`}
|
||||
expandedNodes={expandedNodes}
|
||||
setExpandedNodes={setExpandedNodes}
|
||||
/>
|
||||
</Column>
|
||||
))}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const handleFieldToggle = (path: string) => {
|
||||
setSelectedFields(prev => {
|
||||
const updated = { ...prev, [path]: !prev[path] };
|
||||
|
||||
// If child selected → ensure all its parents are also selected
|
||||
if (updated[path]) {
|
||||
const parts = path.split('.');
|
||||
while (parts.length > 1) {
|
||||
parts.pop();
|
||||
const parent = parts.join('.');
|
||||
if (credential.disclosedKeys.includes(parent)) {
|
||||
updated[parent] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Object.keys(updated).forEach(p => {
|
||||
if (p.startsWith(path + '.') && updated[p]) {
|
||||
updated[p] = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const selectedPaths = Object.keys(updated).filter(p => updated[p]);
|
||||
onDisclosuresChange?.(selectedPaths);
|
||||
return updated;
|
||||
});
|
||||
};
|
||||
|
||||
export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = false, credential, verifiableCredentialData, wellknown, selectable, selected, service, onPress, flow, walletBindingResponse, KEBAB_POPUP, DISMISS, isKebabPopUp, vcMetadata, isInitialLaunch}) => {
|
||||
const wellknownDisplayProperty = new Display(wellknown);
|
||||
const vcSelectableButton =
|
||||
const vcSelectableButton =
|
||||
selectable &&
|
||||
(flow === VCItemContainerFlowType.VP_SHARE ? (
|
||||
<CheckBox
|
||||
@@ -55,8 +210,8 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = fals
|
||||
));
|
||||
const issuerLogo = verifiableCredentialData.issuerLogo;
|
||||
const faceImage = verifiableCredentialData.face;
|
||||
const {start} = useCopilot();
|
||||
const {t} = useTranslation();
|
||||
const { start } = useCopilot();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
@@ -69,12 +224,13 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = fals
|
||||
]}>
|
||||
<View
|
||||
onLayout={
|
||||
isInitialLaunch
|
||||
? () => start(t('copilot:cardTitle'))
|
||||
: undefined
|
||||
isInitialLaunch ? () => start(t('copilot:cardTitle')) : undefined
|
||||
}>
|
||||
<Row crossAlign="center" padding="3 0 0 3">
|
||||
<VcItemContainerProfileImage isPinned={isPinned} verifiableCredentialData={verifiableCredentialData} />
|
||||
<VcItemContainerProfileImage
|
||||
isPinned={isPinned}
|
||||
verifiableCredentialData={verifiableCredentialData}
|
||||
/>
|
||||
<Column fill align={'space-around'} margin="0 10 0 10">
|
||||
<VCItemFieldValue
|
||||
key={'credentialType'}
|
||||
@@ -106,7 +262,7 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = fals
|
||||
<>
|
||||
{!verifiableCredentialData?.vcMetadata.isExpired &&
|
||||
(!walletBindingResponse &&
|
||||
isActivationNeeded(verifiableCredentialData?.issuer)
|
||||
isActivationNeeded(verifiableCredentialData?.issuer)
|
||||
? SvgImage.walletUnActivatedIcon()
|
||||
: SvgImage.walletActivatedIcon())}
|
||||
<Pressable
|
||||
@@ -129,7 +285,66 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = fals
|
||||
</>
|
||||
)}
|
||||
{vcSelectableButton}
|
||||
{flow === VCItemContainerFlowType.VP_SHARE && (credential?.disclosedKeys?.length > 0) && (
|
||||
<Pressable onPress={toggleExpand}>
|
||||
<Icon
|
||||
name={isExpanded ? 'expand-less' : 'expand-more'}
|
||||
color={Theme.Colors.Icon}
|
||||
/>
|
||||
</Pressable>
|
||||
)}
|
||||
</Row>
|
||||
{/* Expanded section for SD-JWT disclosed keys */}
|
||||
{flow === VCItemContainerFlowType.VP_SHARE &&
|
||||
isExpanded &&
|
||||
credential?.disclosedKeys?.length > 0 && (
|
||||
<Column padding="8 0">
|
||||
<View style={{ paddingHorizontal: 6, marginTop: 8 }}>
|
||||
<View
|
||||
style={{...Theme.Styles.horizontalSeparator, marginBottom: 12 }}
|
||||
/>
|
||||
<Column>
|
||||
<Text
|
||||
style={Theme.Styles.disclosureTitle}>
|
||||
{t('SendVPScreen:selectedFieldsTitle')}
|
||||
</Text>
|
||||
<Text
|
||||
style={Theme.Styles.disclosureSubtitle}>
|
||||
{t('SendVPScreen:selectedFieldsSubtitle')}
|
||||
</Text>
|
||||
</Column>
|
||||
|
||||
<Row style={{ marginTop: 12 }} width='100%' align='flex-end'><Pressable onPress={toggleSelectAll}>
|
||||
<Text
|
||||
color={Theme.Colors.Icon}
|
||||
style={Theme.Styles.disclosureSelectButton}>
|
||||
{areAllSelected()
|
||||
? t('SendVPScreen:unselectAll')
|
||||
: t('SendVPScreen:selectAll')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</Row>
|
||||
|
||||
<View
|
||||
style={{ ...Theme.Styles.horizontalSeparator, marginTop: 12 }}
|
||||
/>
|
||||
|
||||
</View>
|
||||
{Object.entries(buildDisclosureTree(credential.disclosedKeys)).map(
|
||||
([name, node]) => (
|
||||
<DisclosureNode
|
||||
key={name}
|
||||
name={name}
|
||||
node={node}
|
||||
fullPath={name}
|
||||
expandedNodes={expandedNodes}
|
||||
setExpandedNodes={setExpandedNodes}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
|
||||
|
||||
<WalletBinding service={service} vcMetadata={vcMetadata} />
|
||||
|
||||
@@ -145,6 +360,23 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({isPinned = fals
|
||||
);
|
||||
};
|
||||
|
||||
function buildDisclosureTree(paths: string[]) {
|
||||
const root: any = {};
|
||||
paths.forEach(path => {
|
||||
const parts = path.split(".");
|
||||
let node = root;
|
||||
parts.forEach((part, idx) => {
|
||||
if (!node[part]) node[part] = { __self: false, children: {} };
|
||||
if (idx === parts.length - 1) {
|
||||
node[part].__self = true;
|
||||
}
|
||||
node = node[part].children;
|
||||
});
|
||||
});
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
export interface VCItemContentProps {
|
||||
context: any;
|
||||
credential: Credential;
|
||||
@@ -165,4 +397,5 @@ export interface VCItemContentProps {
|
||||
isKebabPopUp: boolean;
|
||||
vcMetadata: VCMetadata;
|
||||
isInitialLaunch?: boolean;
|
||||
onDisclosuresChange?: (disclosures: string[]) => void;
|
||||
}
|
||||
@@ -25,9 +25,9 @@ export class VCProcessor {
|
||||
return parseJSON(decodedString);
|
||||
}
|
||||
if(vcFormat === VCFormat.vc_sd_jwt || vcFormat === VCFormat.dc_sd_jwt) {
|
||||
const { fullResolvedPayload, disclosedKeys, publicKeys } =
|
||||
const { fullResolvedPayload, disclosedKeys, publicKeys,pathToDisclosures } =
|
||||
reconstructSdJwtFromCompact(vcData.credential.toString());
|
||||
return {fullResolvedPayload,disclosedKeys,publicKeys};
|
||||
return {fullResolvedPayload,disclosedKeys,publicKeys,pathToDisclosures};
|
||||
}
|
||||
return getVerifiableCredential(vcData);
|
||||
}
|
||||
@@ -61,13 +61,16 @@ export function reconstructSdJwtFromCompact(
|
||||
sdJwtCompact: string,
|
||||
): {
|
||||
fullResolvedPayload: Record<string, any>;
|
||||
disclosedKeys: Set<string>;
|
||||
publicKeys: Set<string>;
|
||||
disclosedKeys: string[];
|
||||
publicKeys: string[];
|
||||
pathToDisclosures: Record<string, string[]>; // Mapof{claimPath -> disclosure strings}
|
||||
} {
|
||||
const sdJwtPublicKeys = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"];
|
||||
const disclosedKeys = new Set<string>();
|
||||
const publicKeys = new Set<string>();
|
||||
const digestToDisclosure: Record<string, any[]> = {};
|
||||
const pathToDisclosures: Record<string, string[]> = {};
|
||||
const digestToDisclosureB64: Record<string, string> = {};
|
||||
|
||||
// Split SD-JWT into parts: [jwt, disclosure1, disclosure2, ...]
|
||||
const parts = sdJwtCompact.trim().split('~');
|
||||
@@ -79,17 +82,23 @@ export function reconstructSdJwtFromCompact(
|
||||
|
||||
// Parse disclosures
|
||||
for (const disclosureB64 of disclosures) {
|
||||
if(disclosureB64.length > 0) {
|
||||
const decodedB64 = disclosureB64.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const decoded = JSON.parse(Buffer.from(decodedB64, 'base64').toString('utf-8'));
|
||||
const digestInput = disclosureB64
|
||||
const digest = base64url(Buffer.from(hashDigest(sdAlg,digestInput)));
|
||||
digestToDisclosure[digest] = decoded;
|
||||
if (disclosureB64.length > 0) {
|
||||
const decodedB64 = disclosureB64.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const decoded = JSON.parse(Buffer.from(decodedB64, 'base64').toString('utf-8'));
|
||||
const digestInput = disclosureB64;
|
||||
const digest = base64url(Buffer.from(hashDigest(sdAlg, digestInput)));
|
||||
|
||||
digestToDisclosure[digest] = decoded;
|
||||
digestToDisclosureB64[digest] = disclosureB64;
|
||||
}
|
||||
}
|
||||
|
||||
//Parse the JWT payload
|
||||
function resolveDisclosures(value: any, path: string = ''): any {
|
||||
//Parse the JWT payload
|
||||
function resolveDisclosures(
|
||||
value: any,
|
||||
path: string = '',
|
||||
parentDisclosures: string[] = [],
|
||||
): any {
|
||||
if (Array.isArray(value)) {
|
||||
return value.flatMap((item, index) => {
|
||||
const currentPath = `${path}[${index}]`;
|
||||
@@ -105,9 +114,11 @@ export function reconstructSdJwtFromCompact(
|
||||
return [];
|
||||
}
|
||||
disclosedKeys.add(currentPath);
|
||||
return [resolveDisclosures(disclosure[1], currentPath)];
|
||||
const currentDisclosures = [...parentDisclosures, digestToDisclosureB64[digest]];
|
||||
pathToDisclosures[currentPath] = currentDisclosures;
|
||||
return [resolveDisclosures(disclosure[1], currentPath, currentDisclosures)];
|
||||
} else {
|
||||
return [resolveDisclosures(item, currentPath)];
|
||||
return [resolveDisclosures(item, currentPath, parentDisclosures)];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -126,13 +137,15 @@ export function reconstructSdJwtFromCompact(
|
||||
if (claimName in value) throw new Error('Overwriting existing key');
|
||||
const fullPath = path ? `${path}.${claimName}` : claimName;
|
||||
disclosedKeys.add(fullPath);
|
||||
result[claimName] = resolveDisclosures(claimValue, fullPath);
|
||||
const currentDisclosures = [...parentDisclosures, digestToDisclosureB64[digest]];
|
||||
pathToDisclosures[fullPath] = currentDisclosures;
|
||||
result[claimName] = resolveDisclosures(claimValue, fullPath, currentDisclosures);
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(value)) {
|
||||
if (k === '_sd') continue;
|
||||
const fullPath = path ? `${path}.${k}` : k;
|
||||
result[k] = resolveDisclosures(v, fullPath);
|
||||
result[k] = resolveDisclosures(v, fullPath, parentDisclosures);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -143,12 +156,18 @@ export function reconstructSdJwtFromCompact(
|
||||
|
||||
// Track public (non-selectively-disclosable) claims
|
||||
for (const key of Object.keys(payload)) {
|
||||
if (key !== '_sd' && key !== '_sd_alg' && sdJwtPublicKeys.includes(key)) {
|
||||
if (key !== '_sd' && key !== '_sd_alg' && sdJwtPublicKeys.includes(key)) {
|
||||
publicKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
const fullResolvedPayload = resolveDisclosures(payload);
|
||||
delete fullResolvedPayload['_sd_alg'];
|
||||
return { fullResolvedPayload, disclosedKeys, publicKeys };
|
||||
|
||||
return {
|
||||
fullResolvedPayload,
|
||||
disclosedKeys: Array.from(disclosedKeys),
|
||||
publicKeys: Array.from(publicKeys),
|
||||
pathToDisclosures,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -286,7 +286,7 @@ function getFullAddress(credential: CredentialSubject) {
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
}
|
||||
const formatKeyLabel = (key: string): string => {
|
||||
export const formatKeyLabel = (key: string): string => {
|
||||
return key
|
||||
.replace(/\[\d+\]/g, '') // Remove [0], [1], etc.
|
||||
.replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase → spaced
|
||||
@@ -303,7 +303,7 @@ const renderFieldRecursively = (
|
||||
parentKey = '',
|
||||
depth = 0,
|
||||
renderedFields: Set<string>,
|
||||
disclosedKeys: Set<string> = new Set(),
|
||||
disclosedKeys: string[],
|
||||
): JSX.Element[] => {
|
||||
const fullKey = parentKey ? `${parentKey}.${key}` : key;
|
||||
let shortKey =
|
||||
@@ -320,12 +320,12 @@ const renderFieldRecursively = (
|
||||
if (Array.isArray(value)) {
|
||||
const label = formatKeyLabel(key);
|
||||
const arrayFullKey = fullKey;
|
||||
const isArrayDisclosed = disclosedKeys.has(arrayFullKey);
|
||||
const isArrayDisclosed = disclosedKeys.includes(arrayFullKey);
|
||||
|
||||
return value.flatMap((item, index) => {
|
||||
const itemKey = `${key}[${index}]`;
|
||||
const itemFullKey = parentKey ? `${parentKey}.${itemKey}` : itemKey;
|
||||
const isItemDisclosed = disclosedKeys.has(itemFullKey);
|
||||
const isItemDisclosed = disclosedKeys.includes(itemFullKey);
|
||||
const showDisclosureIcon = isArrayDisclosed || isItemDisclosed;
|
||||
|
||||
return [
|
||||
@@ -436,7 +436,7 @@ const renderFieldRecursively = (
|
||||
shortKey = publicKeyLabelMap[shortKey];
|
||||
}
|
||||
const label = formatKeyLabel(shortKey);
|
||||
const isDisclosed = disclosedKeys.has(fullKey);
|
||||
const isDisclosed = disclosedKeys.includes(fullKey);
|
||||
return [
|
||||
<Row
|
||||
key={`extra-${fullKey}`}
|
||||
@@ -471,7 +471,7 @@ export const fieldItemIterator = (
|
||||
): JSX.Element[] => {
|
||||
const fieldNameColor = display.getTextColor(Theme.Colors.DetailsLabel);
|
||||
const fieldValueColor = display.getTextColor(Theme.Colors.Details);
|
||||
const disclosedKeys = verifiableCredential.disclosedKeys || new Set<string>();
|
||||
const disclosedKeys = verifiableCredential.disclosedKeys || [];
|
||||
const renderedFields = new Set<string>();
|
||||
|
||||
const renderedMainFields = fields.map(field => {
|
||||
@@ -501,7 +501,7 @@ export const fieldItemIterator = (
|
||||
return null;
|
||||
}
|
||||
|
||||
const isDisclosed = disclosedKeys.has(field);
|
||||
const isDisclosed = disclosedKeys.includes(field);
|
||||
return (
|
||||
<Row
|
||||
key={field}
|
||||
|
||||
@@ -769,6 +769,24 @@ export const DefaultTheme = {
|
||||
flex: 1,
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
horizontalSeparator:{
|
||||
height: 1,
|
||||
backgroundColor: '#DADADA',
|
||||
},
|
||||
disclosureTitle:{
|
||||
fontFamily: 'Inter_700Bold',
|
||||
fontSize: 15,
|
||||
color: Colors.Black,
|
||||
},
|
||||
disclosureSubtitle:{
|
||||
fontSize: 13,
|
||||
color: '#747474',
|
||||
marginTop: 4,
|
||||
},
|
||||
disclosureSelectButton:{
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter_700Bold',
|
||||
}
|
||||
}),
|
||||
BannerStyles: StyleSheet.create({
|
||||
container: {
|
||||
@@ -1173,6 +1191,21 @@ export const DefaultTheme = {
|
||||
borderColor: Colors.Orange,
|
||||
borderRadius: 30,
|
||||
},
|
||||
sharedSuccessfullyVerifierInfo:{
|
||||
alignSelf: 'center',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
sharedSuccessfullyVerifierLogo: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
marginRight: 12,
|
||||
}
|
||||
}),
|
||||
AppMetaDataStyles: StyleSheet.create({
|
||||
buttonContainer: {
|
||||
|
||||
@@ -777,6 +777,25 @@ export const PurpleTheme = {
|
||||
flex: 1,
|
||||
justifyContent: 'space-around',
|
||||
},
|
||||
horizontalSeparator:{
|
||||
height: 1,
|
||||
backgroundColor: '#DADADA',
|
||||
marginBottom: 12,
|
||||
},
|
||||
disclosureTitle:{
|
||||
fontFamily: 'Inter_700Bold',
|
||||
fontSize: 15,
|
||||
color: Colors.Black,
|
||||
},
|
||||
disclosureSubtitle:{
|
||||
fontSize: 13,
|
||||
color: '#747474',
|
||||
marginTop: 4,
|
||||
},
|
||||
disclosureSelectButton:{
|
||||
fontSize: 14,
|
||||
fontFamily: 'Inter_700Bold',
|
||||
}
|
||||
}),
|
||||
BannerStyles: StyleSheet.create({
|
||||
container: {
|
||||
@@ -1178,6 +1197,21 @@ export const PurpleTheme = {
|
||||
borderColor: Colors.Purple,
|
||||
borderRadius: 30,
|
||||
},
|
||||
sharedSuccessfullyVerifierInfo:{
|
||||
alignSelf: 'center',
|
||||
backgroundColor: '#F5F5F5',
|
||||
borderRadius: 16,
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
sharedSuccessfullyVerifierLogo: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
marginRight: 12,
|
||||
}
|
||||
}),
|
||||
AppMetaDataStyles: StyleSheet.create({
|
||||
buttonContainer: {
|
||||
|
||||
Reference in New Issue
Block a user