[INJIMOB-3581] add revocation and reverification logic (#2117)

* [INJIMOB-3581] add revocation and reverification logic

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>

* [INJIMOB-3581] refactor readable array conversion to a shared utility

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>

---------

Signed-off-by: Abhishek Paul <paul.apaul.abhishek.ap@gmail.com>
This commit is contained in:
abhip2565
2025-11-05 19:03:12 +05:30
committed by GitHub
parent a0d25feac1
commit 52c7ed1357
73 changed files with 2083 additions and 517 deletions

View File

@@ -20,6 +20,7 @@ export type ActivityLogType =
| 'WALLET_BINDING_SUCCESSFULL'
| 'WALLET_BINDING_FAILURE'
| 'VC_REMOVED'
| 'VC_STATUS_CHANGED'
| 'TAMPERED_VC_REMOVED';
export interface ActivityLog {
@@ -35,6 +36,7 @@ export class VCActivityLog implements ActivityLog {
type: ActivityLogType;
issuer: string;
flow: string;
vcStatus?: string;
constructor({
id = '',
@@ -46,6 +48,7 @@ export class VCActivityLog implements ActivityLog {
issuer = '',
credentialConfigurationId = '',
flow = VCItemContainerFlowType.VC_SHARE,
vcStatus = '',
} = {}) {
this.id = id;
this.idType = idType;
@@ -56,17 +59,23 @@ export class VCActivityLog implements ActivityLog {
this.issuer = issuer;
this.credentialConfigurationId = credentialConfigurationId;
this.flow = flow;
this.vcStatus = vcStatus;
}
getActionText(t: TFunction, wellknown: Object | undefined) {
const formattedVcStatus = this.vcStatus ? `.${this.vcStatus}` : '';
if (!!this.credentialConfigurationId && wellknown) {
const cardType = getCredentialTypeFromWellKnown(
wellknown,
this.credentialConfigurationId,
);
return `${t(this.type, {idType: cardType})}`;
return `${t(this.type + formattedVcStatus, {
idType: cardType,
vcStatus: this.vcStatus,
})}`;
}
return `${t(this.type, {idType: ''})}`;
return `${t(this.type + formattedVcStatus, {idType: '', vcStatus: this.vcStatus})}`;
}
static getLogFromObject(data: Object): VCActivityLog {

View File

@@ -15,7 +15,7 @@ import {useSettingsScreen} from '../screens/Settings/SettingScreenController';
export const BannerNotificationContainer: React.FC<
BannerNotificationContainerProps
> = props => {
const {showVerificationStatusBanner = true} = props;
const { showVerificationStatusBanner = true } = props;
const scanScreenController = useScanScreen();
const settingsScreenController = useSettingsScreen(props);
const showQuickShareSuccessBanner =
@@ -23,9 +23,11 @@ export const BannerNotificationContainer: React.FC<
const bannerNotificationController = UseBannerNotification();
const WalletBindingSuccess = bannerNotificationController.isBindingSuccess;
const {t} = useTranslation('BannerNotification');
const reverificationSuccessObject = bannerNotificationController.isReverificationSuccess;
const reverificationFailureObject = bannerNotificationController.isReverificationFailed;
const { t } = useTranslation('BannerNotification');
const rt = useTranslation('RequestScreen').t;
const verificationStatus = bannerNotificationController.verificationStatus;
const verificationStatus = bannerNotificationController.verificationStatus || null;
return (
<>
@@ -69,6 +71,20 @@ export const BannerNotificationContainer: React.FC<
</View>
)}
{reverificationSuccessObject.status && (
<View style={Theme.BannerStyles.topBanner}>
<BannerNotification
type={BannerStatusType.SUCCESS}
message={t(`reverifiedSuccessfully.${reverificationSuccessObject.statusValue}`, { vcType: reverificationSuccessObject.vcType })}
onClosePress={
bannerNotificationController.RESET_REVIRIFICATION_SUCCESS
}
key={'reverifiedSuccessfullyPopup'}
testId={'reverifiedSuccessfullyPopup'}
/>
</View>
)}
{showQuickShareSuccessBanner && (
<View style={Theme.BannerStyles.topBanner}>
<BannerNotification
@@ -101,18 +117,6 @@ export const BannerNotificationContainer: React.FC<
/>
)}
{verificationStatus !== null && showVerificationStatusBanner && (
<BannerNotification
type={verificationStatus.statusType}
message={t(`VcVerificationBanner:${verificationStatus?.statusType}`, {
vcDetails: `${verificationStatus.vcType} ${verificationStatus.vcNumber}`,
})}
onClosePress={bannerNotificationController.RESET_VERIFICATION_STATUS}
key={'reVerificationInProgress'}
testId={'reVerificationInProgress'}
/>
)}
{bannerNotificationController.isDownloadingFailed && (
<BannerNotification
type={BannerStatusType.ERROR}
@@ -122,6 +126,16 @@ export const BannerNotificationContainer: React.FC<
testId={'downloadingVcFailedPopup'}
/>
)}
{reverificationFailureObject.status && (
<BannerNotification
type={BannerStatusType.ERROR}
message={t(`reverificationFailed.${reverificationFailureObject.statusValue}`, { vcType: reverificationFailureObject.vcType })}
onClosePress={bannerNotificationController.RESET_REVERIFICATION_FAILURE}
key={'reverificationFailedPopup'}
testId={'reverificationFailedPopup'}
/>
)}
{bannerNotificationController.isDownloadingSuccess && (
<BannerNotification
type={BannerStatusType.SUCCESS}
@@ -137,6 +151,8 @@ export const BannerNotificationContainer: React.FC<
export type vcVerificationBannerDetails = {
statusType: BannerStatus;
isRevoked: boolean;
isExpired: boolean;
vcType: string;
};

View File

@@ -10,6 +10,8 @@ import {VcMetaEvents} from '../machines/VerifiableCredential/VCMetaMachine/VCMet
import {
selectIsDownloadingFailed,
selectIsDownloadingSuccess,
selectIsReverificationFailure,
selectIsReverificationSuccess,
selectWalletBindingSuccess,
} from '../machines/VerifiableCredential/VCMetaMachine/VCMetaSelectors';
import {selectVerificationStatus} from '../machines/VerifiableCredential/VCItemMachine/VCItemSelectors';
@@ -27,6 +29,8 @@ export const UseBannerNotification = () => {
isBiometricUnlock: useSelector(settingsService, selectIsBiometricUnlock),
isDownloadingSuccess: useSelector(vcMetaService, selectIsDownloadingSuccess),
isDownloadingFailed: useSelector(vcMetaService, selectIsDownloadingFailed),
isReverificationSuccess: useSelector(vcMetaService,selectIsReverificationSuccess),
isReverificationFailed: useSelector(vcMetaService, selectIsReverificationFailure),
DISMISS: () => {
settingsService.send(SettingsEvents.DISMISS());
},
@@ -40,5 +44,11 @@ export const UseBannerNotification = () => {
RESET_DOWNLOADING_SUCCESS: () => {
vcMetaService.send(VcMetaEvents.RESET_DOWNLOADING_SUCCESS());
},
RESET_REVIRIFICATION_SUCCESS: () => {
vcMetaService.send(VcMetaEvents.RESET_REVERIFY_VC_SUCCESS());
},
RESET_REVERIFICATION_FAILURE: () => {
vcMetaService.send(VcMetaEvents.RESET_REVERIFY_VC_FAILED());
},
};
};

View File

@@ -1,20 +1,20 @@
import React from 'react';
import {Icon, ListItem, Overlay} from 'react-native-elements';
import {Theme} from '../components/ui/styleUtils';
import {Column, Row, Text} from '../components/ui';
import {View} from 'react-native';
import {useKebabPopUp} from './KebabPopUpController';
import {ActorRefFrom} from 'xstate';
import {useTranslation} from 'react-i18next';
import {FlatList} from 'react-native-gesture-handler';
import {VCMetadata} from '../shared/VCMetadata';
import { Icon, ListItem, Overlay } from 'react-native-elements';
import { Theme } from '../components/ui/styleUtils';
import { Column, Row, Text } from '../components/ui';
import { View } from 'react-native';
import { useKebabPopUp } from './KebabPopUpController';
import { ActorRefFrom } from 'xstate';
import { useTranslation } from 'react-i18next';
import { FlatList } from 'react-native-gesture-handler';
import { VCMetadata } from '../shared/VCMetadata';
import testIDProps from '../shared/commonUtil';
import {getKebabMenuOptions} from './kebabMenuUtils';
import {VCItemMachine} from '../machines/VerifiableCredential/VCItemMachine/VCItemMachine';
import { getKebabMenuOptions } from './kebabMenuUtils';
import { VCItemMachine } from '../machines/VerifiableCredential/VCItemMachine/VCItemMachine';
export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
const controller = useKebabPopUp(props);
const {t} = useTranslation('HomeScreenKebabPopUp');
const { t } = useTranslation('HomeScreenKebabPopUp');
return (
<Column>
@@ -52,14 +52,14 @@ export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
<FlatList
data={getKebabMenuOptions(props)}
renderItem={({item}) => (
renderItem={({ item }) => (
<ListItem topDivider onPress={item.onPress}>
<Row crossAlign="center" style={{flex: 1}}>
<View style={{width: 25, alignItems: 'center'}}>
<Row crossAlign="center" style={{ flex: 1 }}>
<View style={{ width: 25, alignItems: 'center' }}>
{item.icon}
</View>
<Text
style={{fontFamily: 'Inter_600SemiBold'}}
style={{ fontFamily: 'Inter_600SemiBold' }}
color={
item.testID === 'removeFromWallet'
? Theme.Colors.warningText
@@ -69,6 +69,7 @@ export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
margin="0 0 0 10">
{item.label}
</Text>
{item.label === t('reverify') && (<View style={Theme.KebabPopUpStyles.new}><Text color='white' weight='bold' style={{ fontSize: 10 }}>{t('new')}</Text></View>)}
</Row>
</ListItem>
)}

View File

@@ -71,6 +71,7 @@ export function useKebabPopUp(props) {
DISMISS: () => service.send(VCItemEvents.DISMISS()),
CANCEL: () => service.send(VCItemEvents.CANCEL()),
SHOW_ACTIVITY: () => service.send(VCItemEvents.SHOW_ACTIVITY()),
REVERIFY_VC: () => service.send(VCItemEvents.REVERIFY_VC()),
INPUT_OTP: (otp: string) => service.send(VCItemEvents.INPUT_OTP(otp)),
RESEND_OTP: () => service.send(VCItemEvents.RESEND_OTP()),
GOTO_SCANSCREEN: () => {

View File

@@ -1,73 +1,123 @@
import React, {ReactElement} from 'react';
import {useTranslation} from 'react-i18next';
import {Dimensions, StyleSheet} from 'react-native';
import {LinearProgress, Overlay} from 'react-native-elements';
import {Button, Column, Text} from './ui';
import {Theme} from './ui/styleUtils';
import React, { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet, View } from 'react-native';
import { LinearProgress, Overlay } from 'react-native-elements';
import { Button, Column, Text } from './ui';
import { Theme } from './ui/styleUtils';
import Svg, { Mask, Rect } from 'react-native-svg';
export const MessageOverlay: React.FC<MessageOverlayProps> = props => {
const {t} = useTranslation('common');
const { t } = useTranslation('common');
const style = StyleSheet.create({
customHeight: {
minHeight: props.minHeight ? props.minHeight : props.progress ? 100 : 170,
},
});
const isHighlightMode = props.overlayMode === 'highlight';
return (
<Overlay
isVisible={props.isVisible}
overlayStyle={Theme.MessageOverlayStyles.overlay}
fullScreen={isHighlightMode}
backdropStyle={{ backgroundColor: isHighlightMode ? 'transparent' : 'rgba(0,0,0,0.6)' }}
overlayStyle={
isHighlightMode
? { backgroundColor: 'transparent', padding: 0, margin: 0 }
: Theme.MessageOverlayStyles.overlay
}
onShow={props.onShow}
onBackdropPress={props.onBackdropPress}>
<Column
testID={props.testID}
width={Dimensions.get('screen').width * 0.8}
style={[Theme.MessageOverlayStyles.popupOverLay, style.customHeight]}>
<Column padding="21" crossAlign="center">
{props.title && (
<Text
testID={props.testID && props.testID + 'Title'}
style={{paddingTop: 3}}
align="center"
weight="bold"
margin="0 0 10 0"
color={Theme.Colors.Details}>
{props.title}
</Text>
{isHighlightMode ? (
<View
style={{ flex: 1 }}
onStartShouldSetResponder={() => true}
onResponderRelease={props.onBackdropPress}
>
{props.cardLayout && (
<>
<Svg
style={StyleSheet.absoluteFill}
pointerEvents="none"
>
<Mask id="hole">
<Rect width="100%" height="100%" fill="white" />
<Rect
x={props.cardLayout.x - 10}
y={props.cardLayout.y - 10}
width={props.cardLayout.width + 20}
height={props.cardLayout.height + 20}
rx={12}
fill="black"
/>
</Mask>
<Rect
width="100%"
height="100%"
fill="rgba(0,0,0,0.65)"
mask="url(#hole)"
/>
</Svg>
</>
)}
{props.message && (
<Text
testID={props.testID && props.testID + 'Message'}
align="center"
weight="semibold"
size="small"
margin="10 0 12 0"
color={Theme.Colors.Details}>
{props.message}
</Text>
)}
{props.progress && <Progress progress={props.progress} />}
{props.hint && (
<Text
testID={props.testID && props.testID + 'Hint'}
size="smaller"
color={Theme.Colors.textLabel}
margin={[4, 0, 0, 0]}>
{props.hint}
</Text>
)}
{props.children}
</Column>
{!props.children && props.onButtonPress ? (
<Button
testID="cancel"
type="gradient"
title={props.buttonText ? t(props.buttonText) : t('cancel')}
onPress={props.onButtonPress}
styles={Theme.MessageOverlayStyles.button}
/>
) : null}
</Column>
</View>
)
: (
(props.title || props.message || props.children) && (
<Column
testID={props.testID}
width={Dimensions.get('screen').width * 0.8}
style={[
Theme.MessageOverlayStyles.popupOverLay,
style.customHeight,
]}>
<Column padding="21" crossAlign="center">
{props.title && (
<Text
testID={props.testID && props.testID + 'Title'}
style={{ paddingTop: 3 }}
align="center"
weight="bold"
margin="0 0 10 0"
color={Theme.Colors.Details}>
{props.title}
</Text>
)}
{props.message && (
<Text
testID={props.testID && props.testID + 'Message'}
align="center"
weight="semibold"
size="small"
margin="10 0 12 0"
color={Theme.Colors.Details}>
{props.message}
</Text>
)}
{props.progress && <Progress progress={props.progress} />}
{props.hint && (
<Text
testID={props.testID && props.testID + 'Hint'}
size="smaller"
color={Theme.Colors.textLabel}
margin={[4, 0, 0, 0]}>
{props.hint}
</Text>
)}
{props.children}
</Column>
{!props.children && props.onButtonPress ? (
<Button
testID="cancel"
type="gradient"
title={props.buttonText ? t(props.buttonText) : t('cancel')}
onPress={props.onButtonPress}
styles={Theme.MessageOverlayStyles.button}
/>
) : null}
</Column>
)
)}
</Overlay>
);
};
@@ -79,7 +129,7 @@ export const ErrorMessageOverlay: React.FC<ErrorMessageOverlayProps> = ({
onDismiss,
translationPath,
}) => {
const {t} = useTranslation(translationPath);
const { t } = useTranslation(translationPath);
return (
<MessageOverlay
@@ -100,6 +150,7 @@ export interface ErrorMessageOverlayProps {
testID?: string;
}
const Progress: React.FC<MessageOverlayProps> = props => {
return typeof props.progress === 'boolean' ? (
props.progress && (
@@ -113,6 +164,7 @@ const Progress: React.FC<MessageOverlayProps> = props => {
export interface MessageOverlayProps {
testID?: string;
isVisible: boolean;
overlayMode?: 'popup' | 'highlight';
title?: string;
buttonText?: string;
message?: string;
@@ -126,6 +178,13 @@ export interface MessageOverlayProps {
onShow?: () => void;
minHeight?: number | string | undefined;
children?: ReactElement<any, any>;
cardLayout?: {
x: number;
y: number;
width: number;
height: number;
type: 'success' | 'failure';
};
}
export interface VCSharingErrorStatusProps {

View File

@@ -3,14 +3,18 @@ import {View} from 'react-native';
import {Icon} from 'react-native-elements';
import {Theme} from './ui/styleUtils';
const PendingIcon: React.FC = () => {
interface PendingIconProps {
color?: string;
}
const PendingIcon: React.FC<PendingIconProps> = (props) => {
return (
<View style={Theme.Styles.verificationStatusIconContainer}>
<View style={Theme.Styles.verificationStatusIconInner}>
<Icon
name="alert-circle"
type="material-community"
color={Theme.Colors.PendingIcon}
color={props.color}
size={12}
/>
</View>

View File

@@ -7,6 +7,7 @@ import {
selectWalletBindingResponse,
selectVerifiableCredentialData,
selectCredential,
isReverifyingVc,
} from '../../machines/VerifiableCredential/VCItemMachine/VCItemSelectors';
import {useInterpret, useSelector} from '@xstate/react';
import {
@@ -49,6 +50,7 @@ export function useVcItemController(vcMetadata: VCMetadata) {
VCItemService,
selectIsSavingFailedInIdle,
),
isReverifyingVc: useSelector(VCItemService, isReverifyingVc),
storeErrorTranslationPath: 'errors.savingFailed',
generatedOn: useSelector(VCItemService, selectGeneratedOn),
isTourGuide: useSelector(authService, selectIsTourGuide),

View File

@@ -1,21 +1,21 @@
import * as React from 'react';
import {useEffect, useState} from 'react';
import {Pressable} from 'react-native';
import {ActorRefFrom} from 'xstate';
import {ErrorMessageOverlay} from '../../MessageOverlay';
import {Theme} from '../../ui/styleUtils';
import {VCMetadata} from '../../../shared/VCMetadata';
import {format} from 'date-fns';
import { useEffect, useRef, useState } from 'react';
import { Pressable, View } from 'react-native';
import { ActorRefFrom } from 'xstate';
import { ErrorMessageOverlay, MessageOverlay } from '../../MessageOverlay';
import { Theme } from '../../ui/styleUtils';
import { VCMetadata } from '../../../shared/VCMetadata';
import { format } from 'date-fns';
import {VCCardSkeleton} from '../common/VCCardSkeleton';
import {VCCardViewContent} from './VCCardViewContent';
import {useVcItemController} from '../VCItemController';
import {getCredentialIssuersWellKnownConfig} from '../../../shared/openId4VCI/Utils';
import {CARD_VIEW_DEFAULT_FIELDS, isVCLoaded} from '../common/VCUtils';
import {VCItemMachine} from '../../../machines/VerifiableCredential/VCItemMachine/VCItemMachine';
import {useTranslation} from 'react-i18next';
import {Copilot} from '../../ui/Copilot';
import {VCProcessor} from '../common/VCProcessor';
import { VCCardSkeleton } from '../common/VCCardSkeleton';
import { VCCardViewContent } from './VCCardViewContent';
import { useVcItemController } from '../VCItemController';
import { getCredentialIssuersWellKnownConfig } from '../../../shared/openId4VCI/Utils';
import { CARD_VIEW_DEFAULT_FIELDS, isVCLoaded } from '../common/VCUtils';
import { VCItemMachine } from '../../../machines/VerifiableCredential/VCItemMachine/VCItemMachine';
import { useTranslation } from 'react-i18next';
import { Copilot } from '../../ui/Copilot';
import { VCProcessor } from '../common/VCProcessor';
export const VCCardView: React.FC<VCItemProps> = ({
vcMetadata,
@@ -28,15 +28,17 @@ export const VCCardView: React.FC<VCItemProps> = ({
isInitialLaunch = false,
isTopCard = false,
onDisclosuresChange,
onMeasured,
}) => {
const controller = useVcItemController(vcMetadata);
const {t} = useTranslation();
const { t } = useTranslation();
const cardRef = useRef<View>(null);
const service = controller.VCItemService;
const verifiableCredentialData = controller.verifiableCredentialData;
const generatedOn = -controller.generatedOn;
let formattedDate =
const formattedDate =
generatedOn && format(new Date(generatedOn), 'MM/dd/yyyy');
useEffect(() => {
@@ -47,6 +49,21 @@ export const VCCardView: React.FC<VCItemProps> = ({
const [wellknown, setWellknown] = useState(null);
const [vc, setVc] = useState(null);
useEffect(() => {
if (onMeasured && cardRef.current) {
const handle = requestAnimationFrame(() => {
cardRef.current?.measureInWindow((x, y, width, height) => {
if (width > 0 && height > 0) {
onMeasured({ x, y, width, height });
}
});
});
return () => cancelAnimationFrame(handle);
}
}, [onMeasured]);
useEffect(() => {
async function loadVc() {
if (!isDownloading) {
@@ -82,7 +99,7 @@ export const VCCardView: React.FC<VCItemProps> = ({
setFields(response.fields);
})
.catch(error => {
setWellknown({fallback: 'true'});
setWellknown({ fallback: 'true' });
console.error(
'Error occurred while fetching wellknown for viewing VC ',
error,
@@ -126,8 +143,15 @@ export const VCCardView: React.FC<VCItemProps> = ({
);
return (
<React.Fragment>
<>
<MessageOverlay
progress={true}
title={t('In Progress')}
isVisible={controller.isReverifyingVc}
/>
<Pressable
ref={cardRef}
accessible={false}
onPress={() => onPress(service)}
style={
@@ -145,7 +169,7 @@ export const VCCardView: React.FC<VCItemProps> = ({
onDismiss={controller.DISMISS}
translationPath={'VcDetails'}
/>
</React.Fragment>
</>
);
};
@@ -162,4 +186,5 @@ export interface VCItemProps {
isInitialLaunch?: boolean;
isTopCard?: boolean;
onDisclosuresChange?: (paths: string[]) => void;
onMeasured?: (rect: { x: number; y: number; width: number; height: number }) => void;
}

View File

@@ -244,6 +244,7 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({
<VCVerification
vcMetadata={verifiableCredentialData?.vcMetadata}
display={wellknownDisplayProperty}
showLastChecked={false}
/>
</Row>
</Column>
@@ -301,7 +302,7 @@ export const VCCardViewContent: React.FC<VCItemContentProps> = ({
<Column padding="8 0">
<View style={{ paddingHorizontal: 6, marginTop: 8 }}>
<View
style={{...Theme.Styles.horizontalSeparator, marginBottom: 12 }}
style={{ ...Theme.Styles.horizontalSeparator, marginBottom: 12 }}
/>
<Column>
<Text

View File

@@ -1,11 +1,13 @@
import {Dimensions, View} from 'react-native';
import {Column, Row, Text} from '../../ui';
import {CustomTooltip} from '../../ui/ToolTip';
import {Theme} from '../../ui/styleUtils';
import { Dimensions, View } from 'react-native';
import { Column, Row, Text } from '../../ui';
import { CustomTooltip } from '../../ui/ToolTip';
import { Theme } from '../../ui/styleUtils';
import React from 'react';
import {SvgImage} from '../../ui/svg';
import {useTranslation} from 'react-i18next';
import { SvgImage } from '../../ui/svg';
import { useTranslation } from 'react-i18next';
import Icon from 'react-native-vector-icons/FontAwesome';
import { STATUS_FIELD_NAME } from './VCUtils';
import { StatusTooltipContent } from './VcStatustooTip';
export const VCItemFieldName = ({
fieldName,
@@ -18,7 +20,7 @@ export const VCItemFieldName = ({
fieldNameColor?: string;
isDisclosed?: boolean;
}) => {
const {t} = useTranslation('ViewVcModal');
const { t } = useTranslation('ViewVcModal');
return (
<Row>
{fieldName && (
@@ -26,64 +28,24 @@ export const VCItemFieldName = ({
testID={`${testID}Title`}
color={textColor}
style={Theme.Styles.fieldItemTitle}>
{fieldName}
{fieldName === STATUS_FIELD_NAME ? t('VcDetails:status') : fieldName}
</Text>
)}
{fieldName == t('VcDetails:status') && (
{fieldName == STATUS_FIELD_NAME && (
<CustomTooltip
testID="statusToolTip"
width={Dimensions.get('screen').width * 0.8}
height={Dimensions.get('screen').height * 0.28}
triggerComponent={SvgImage.info()}
triggerComponentStyles={{marginLeft: 2, marginTop: 2}}
triggerComponentStyles={{ marginLeft: 2, marginTop: 2 }}
toolTipContent={
<Column align="flex-start">
<View style={{marginBottom: 20}}>
<Text weight="semibold">
{t('statusToolTipContent.valid.title')}
</Text>
<Text
weight="regular"
style={[
Theme.Styles.tooltipContentDescription,
{marginTop: 3},
]}>
{t('statusToolTipContent.valid.description')}
</Text>
</View>
<View style={{marginBottom: 20}}>
<Text weight="semibold">
{t('statusToolTipContent.pending.title')}
</Text>
<Text
weight="regular"
style={[
Theme.Styles.tooltipContentDescription,
{marginTop: 3},
]}>
{t('statusToolTipContent.pending.description')}
</Text>
</View>
<View>
<Text weight="semibold">
{t('statusToolTipContent.expired.title')}
</Text>
<Text
weight="regular"
style={[
Theme.Styles.tooltipContentDescription,
{marginTop: 3},
]}>
{t('statusToolTipContent.expired.description')}
</Text>
</View>
</Column>
<StatusTooltipContent />
}
/>
)}
{isDisclosed && (
<Icon name="share-square-o" size={10} color="#666" style={{marginLeft:5, marginTop:3}} />
<Icon name="share-square-o" size={10} color="#666" style={{ marginLeft: 5, marginTop: 3 }} />
)}
</Row>
);
@@ -94,19 +56,28 @@ export const VCItemFieldValue = ({
testID,
fieldValueColor: textColor = Theme.Colors.Details,
}: {
fieldValue: string;
fieldValue: any;
testID: string;
fieldValueColor?: string;
}) => {
return (
<>
<Text
if (React.isValidElement(fieldValue)) {
return (
<View
testID={`${testID}Value`}
color={textColor}
style={Theme.Styles.fieldItemValue}>
>
{fieldValue}
</Text>
</>
</View>
);
}
return (
<Text
testID={`${testID}Value`}
color={textColor}
style={Theme.Styles.fieldItemValue}>
{fieldValue}
</Text>
);
};

View File

@@ -33,6 +33,9 @@ export const DETAIL_VIEW_DEFAULT_FIELDS = [
'address',
];
export const STATUS_FIELD_NAME = 'Status';
export const VC_STATUS_KEYS = ['valid', 'pending', 'expired', 'revoked'];
//todo UIN & VID to be removed once we get the fields in the wellknown endpoint
export const CARD_VIEW_ADD_ON_FIELDS = ['UIN', 'VID'];
export const DETAIL_VIEW_ADD_ON_FIELDS = [

View File

@@ -0,0 +1,30 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { Column } from "../../ui";
import { Theme } from "../../ui/styleUtils";
import { Text } from "../../ui";
import { VC_STATUS_KEYS } from "./VCUtils";
export const StatusTooltipContent = () => {
const { t } = useTranslation('ViewVcModal');
return (
<Column align="center" style={{marginTop:20}}>
{VC_STATUS_KEYS.map(key => (
<View key={key} style={{ marginBottom: 20 }}>
<Text weight="semibold">{t(`statusToolTipContent.${key}.title`)}</Text>
<Text
weight="regular"
style={[
Theme.Styles.tooltipContentDescription,
{ marginTop: 3 },
]}>
{t(`statusToolTipContent.${key}.description`)}
</Text>
</View>
))}
</Column>
);
};

View File

@@ -1,53 +1,83 @@
import testIDProps from '../shared/commonUtil';
import {Display} from './VC/common/VCUtils';
import VerifiedIcon from './VerifiedIcon';
import {Row, Text} from './ui';
import {Theme} from './ui/styleUtils';
import React from 'react';
import {useTranslation} from 'react-i18next';
import { View } from 'react-native';
import testIDProps from '../shared/commonUtil';
import { Display } from './VC/common/VCUtils';
import VerifiedIcon from './VerifiedIcon';
import PendingIcon from './PendingIcon';
import {VCMetadata} from '../shared/VCMetadata';
import { Row, Text } from './ui';
import { Theme } from './ui/styleUtils';
import { useTranslation } from 'react-i18next';
import { VCMetadata } from '../shared/VCMetadata';
export const VCVerification: React.FC<VCVerificationProps> = ({
vcMetadata,
display,
showLastChecked = true,
}) => {
const {t} = useTranslation('VcDetails');
const statusText = vcMetadata.isVerified
? vcMetadata.isExpired
? t('expired')
: t('valid')
: t('pending');
const { t } = useTranslation('VcDetails');
let statusText: string;
let statusIcon: JSX.Element;
if (vcMetadata.isVerified) {
if (vcMetadata.isRevoked) {
statusText = t('revoked');
statusIcon = <PendingIcon color="brown" />;
} else if (vcMetadata.isExpired) {
statusText = t('expired');
statusIcon = <PendingIcon color="red" />;
} else {
statusText = t('valid');
statusIcon = <VerifiedIcon />;
}
} else {
statusText = t('pending');
statusIcon = <PendingIcon color="orange" />;
}
const statusIcon = vcMetadata.isVerified ? (
vcMetadata.isExpired ? (
<PendingIcon />
) : (
<VerifiedIcon />
)
) : (
<PendingIcon />
);
return (
<Row
{...testIDProps('verified')}
style={{
alignItems: 'center',
}}>
<React.Fragment>
{statusIcon}
<View
{...testIDProps('verified')}
style={{
flexDirection: 'column',
alignItems: 'flex-start',
paddingVertical: 6,
}}>
{/* First Row: Status Icon + Text */}
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
{statusIcon}
<Text
testID="verificationStatus"
color={display.getTextColor(Theme.Colors.Details)}
style={Theme.Styles.verificationStatus}>
{statusText}
</Text>
</View>
{showLastChecked && vcMetadata.lastKnownStatusTimestamp && (
<View style={{ marginTop: 4 }}>
<Text
testID="verificationStatus"
testID='lastCheckedLabel'
color={display.getTextColor(Theme.Colors.Details)}
style={Theme.Styles.verificationStatus}>
{statusText}
style={[Theme.Styles.verificationStatus, { fontFamily: 'Inter_400' }]}>
{t('lastChecked')}
</Text>
</React.Fragment>
</Row>
);
<Text
testID="lastKnownStatusTimestamp"
color={display.getTextColor(Theme.Colors.Details)}
style={[Theme.Styles.verificationStatus,{ fontFamily: 'Inter_400' }]}>
{new Date(vcMetadata.lastKnownStatusTimestamp).toLocaleString()}
</Text>
</View>
)}
</View>
);
};
export interface VCVerificationProps {
vcMetadata: VCMetadata;
display: Display;
showLastChecked?: boolean;
}

View File

@@ -31,6 +31,12 @@ export const getKebabMenuOptions = props => {
onPress: controller.SHOW_ACTIVITY,
testID: 'viewActivityLog',
},
{
label: t('reverify'),
icon: SvgImage.ReverifyIcon(),
onPress: controller.REVERIFY_VC,
testID: 'reverify',
},
{
label: t('removeFromWallet'),
icon: SvgImage.outlinedDeleteIcon(),

View File

@@ -60,6 +60,7 @@ import QuestionIcon from '../../assets/questionIcon.svg';
import CopyIcon from '../../assets/file_copy.svg';
import StarIcon from '../../assets/credentialRegestryStar.svg';
import SelectedCheckBox from '../../assets/Selected_Check_Box.svg';
import ReverifyIcon from '../../assets/Reverify.svg';
export class SvgImage {
static selectedCheckBox() {
return <SelectedCheckBox />;
@@ -248,6 +249,12 @@ export class SvgImage {
);
}
static ReverifyIcon() {
return (
<ReverifyIcon/>
)
}
static OutlinedPinIcon() {
return <OutlinedPinIcon {...testIDProps('outlinedPinIcon')} />;
}

View File

@@ -769,24 +769,24 @@ export const DefaultTheme = {
flex: 1,
justifyContent: 'space-around',
},
horizontalSeparator:{
horizontalSeparator: {
height: 1,
backgroundColor: '#DADADA',
},
disclosureTitle:{
disclosureTitle: {
fontFamily: 'Inter_700Bold',
fontSize: 15,
color: Colors.Black,
},
disclosureSubtitle:{
disclosureSubtitle: {
fontSize: 13,
color: '#747474',
marginTop: 4,
},
disclosureSelectButton:{
disclosureSelectButton: {
fontSize: 14,
fontFamily: 'Inter_700Bold',
}
},
}),
BannerStyles: StyleSheet.create({
container: {
@@ -1191,7 +1191,7 @@ export const DefaultTheme = {
borderColor: Colors.Orange,
borderRadius: 30,
},
sharedSuccessfullyVerifierInfo:{
sharedSuccessfullyVerifierInfo: {
alignSelf: 'center',
backgroundColor: '#F5F5F5',
borderRadius: 16,
@@ -1205,7 +1205,7 @@ export const DefaultTheme = {
height: 40,
borderRadius: 8,
marginRight: 12,
}
},
}),
AppMetaDataStyles: StyleSheet.create({
buttonContainer: {
@@ -1366,6 +1366,15 @@ export const DefaultTheme = {
position: 'absolute',
bottom: 0,
},
new: {
height: 20,
width: 'auto',
backgroundColor: '#FF5300',
alignItems: 'center',
marginLeft: 10,
borderRadius: 5,
paddingHorizontal: 5,
},
kebabHeaderStyle: {
justifyContent: 'space-between',
fontFamily: 'Inter_700Bold',
@@ -2168,14 +2177,14 @@ export const DefaultTheme = {
color: '#973C00',
marginBottom: 5,
},
noteDescriptionText:{
noteDescriptionText: {
fontSize: 13,
color: '#973C00',
fontFamily: 'Inter_400Regular',
lineHeight: 18,
textAlign: 'left',
marginLeft: -25
}
marginLeft: -25,
},
}),
DisclosureInfo: StyleSheet.create({
view: {

View File

@@ -1372,6 +1372,15 @@ export const PurpleTheme = {
position: 'absolute',
bottom: 0,
},
new: {
height: 20,
width: 'auto',
backgroundColor: '#FF5300',
alignItems: 'center',
marginLeft: 10,
borderRadius: 5,
paddingHorizontal: 5,
},
kebabHeaderStyle: {
justifyContent: 'space-between',
fontFamily: 'Inter_700Bold',