Files
inji-wallet/screens/Home/MyVcsTab.tsx
srikanth716 f3b500d56b --amend
Signed-off-by: srikanth716 <srikanthsri7447@gmail.com>
2024-05-28 09:42:53 +05:30

487 lines
16 KiB
TypeScript

import React, {useEffect, useState} from 'react';
import {Button, Column, Row, Text} from '../../components/ui';
import {Theme} from '../../components/ui/styleUtils';
import {Dimensions, Pressable, RefreshControl, View} from 'react-native';
import {useMyVcsTab} from './MyVcsTabController';
import {HomeScreenTabProps} from './HomeScreen';
import {AddVcModal} from './MyVcs/AddVcModal';
import {GetVcModal} from './MyVcs/GetVcModal';
import {useTranslation} from 'react-i18next';
import {GET_INDIVIDUAL_ID} from '../../shared/constants';
import {MessageOverlay} from '../../components/MessageOverlay';
import {VcItemContainer} from '../../components/VC/VcItemContainer';
import {
BannerNotification,
BannerStatusType,
} from '../../components/BannerNotification';
import {
getErrorEventData,
sendErrorEvent,
} from '../../shared/telemetry/TelemetryUtils';
import {TelemetryConstants} from '../../shared/telemetry/TelemetryConstants';
import {Error} from '../../components/ui/Error';
import {useFocusEffect, useIsFocused} from '@react-navigation/native';
import {getVCsOrderedByPinStatus} from '../../shared/Utils';
import {SvgImage} from '../../components/ui/svg';
import {SearchBar} from '../../components/ui/SearchBar';
import {Icon} from 'react-native-elements';
import {VCMetadata} from '../../shared/VCMetadata';
import {CopilotStep, useCopilot, walkthroughable} from 'react-native-copilot';
export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
const {t} = useTranslation('MyVcsTab');
const controller = useMyVcsTab(props);
const vcMetadataOrderedByPinStatus = getVCsOrderedByPinStatus(
controller.vcMetadatas,
);
const [clearSearchIcon, setClearSearchIcon] = useState(false);
const [search, setSearch] = useState('');
const [filteredSearchData, setFilteredSearchData] = useState<
Array<Record<string, VCMetadata>>
>([]);
const [showPinVc, setShowPinVc] = useState(true);
const getId = () => {
controller.DISMISS();
controller.GET_VC();
};
const clearIndividualId = () => {
GET_INDIVIDUAL_ID({id: '', idType: 'UIN'});
};
const onFocusSearch = () => {
setShowPinVc(false);
};
const clearSearchText = () => {
filterVcs('');
setClearSearchIcon(false);
setShowPinVc(true);
};
const {start, goToNth} = useCopilot();
useEffect(() => {
filterVcs(search);
}, [controller.vcData]);
const filterVcs = (searchText: string) => {
setSearch(searchText);
setFilteredSearchData([]);
const searchTextLower = searchText.toLowerCase();
const filteredData: Array<Record<string, VCMetadata>> = [];
for (const [vcKey, vc] of Object.entries(controller.vcData)) {
const isDownloading = vc === null;
if (!isDownloading) {
let isVcFound = false;
const credentialSubject =
vc.verifiableCredential.credentialSubject ||
vc.verifiableCredential.credential.credentialSubject;
if (credentialSubject) {
isVcFound = searchNestedCredentialFields(
searchTextLower,
credentialSubject,
);
}
if (isVcFound) {
filteredData.push({[vcKey]: vc['vcMetadata']});
}
}
}
setFilteredSearchData(filteredData);
const isSearchNotEmpty = searchText !== '';
setClearSearchIcon(isSearchNotEmpty);
setShowPinVc(!isSearchNotEmpty);
};
const searchNestedCredentialFields = (
searchText: string,
credentialSubjectData: any,
): boolean => {
for (const [credentialKey, credentialValue] of Object.entries(
credentialSubjectData,
)) {
if (shouldSkip(credentialKey)) {
continue;
}
if (isStringAndContains(searchText, credentialValue)) {
return true;
}
if (
isObjectAndNotNull(credentialValue) &&
searchNestedCredentialFields(searchText, credentialValue)
) {
return true;
}
}
return false;
};
const shouldSkip = (key: string): boolean => {
return key === 'biometrics' || key === 'id' || key === 'vcVer';
};
const isStringAndContains = (searchText: string, value: any): boolean => {
return (
typeof value === 'string' && value.toLowerCase().includes(searchText)
);
};
const isObjectAndNotNull = (value: any): boolean => {
return typeof value === 'object' && value !== null;
};
useEffect(() => {
if (controller.areAllVcsLoaded) {
controller.RESET_STORE_VC_ITEM_STATUS();
controller.RESET_IN_PROGRESS_VCS_DOWNLOADED();
}
if (controller.inProgressVcDownloads?.size > 0) {
controller.SET_STORE_VC_ITEM_STATUS();
}
if (controller.showHardwareKeystoreNotExistsAlert) {
sendErrorEvent(
getErrorEventData(
TelemetryConstants.FlowType.appOnboarding,
TelemetryConstants.ErrorId.doesNotExist,
TelemetryConstants.ErrorMessage.hardwareKeyStore,
),
);
}
if (controller.isTampered) {
sendErrorEvent(
getErrorEventData(
TelemetryConstants.FlowType.appLogin,
TelemetryConstants.ErrorId.vcsAreTampered,
TelemetryConstants.ErrorMessage.vcsAreTampered,
),
);
}
}, [
controller.areAllVcsLoaded,
controller.inProgressVcDownloads,
controller.isTampered,
]);
useFocusEffect(
React.useCallback(() => {
filterVcs('');
}, []),
);
let failedVCsList = [];
controller.downloadFailedVcs.forEach(vc => {
failedVCsList.push(`\n${vc.idType}:${vc.id}`);
});
const isVerificationFailed = controller.verificationErrorMessage !== '';
const verificationErrorMessage = t(
`errors.verificationFailed.${controller.verificationErrorMessage}`,
);
const downloadFailedVcsErrorMessage = `${t(
'errors.downloadLimitExpires.message',
)}${failedVCsList}`;
const isDownloadFailedVcs =
useIsFocused() &&
controller.downloadFailedVcs.length >= 1 &&
!controller.AddVcModalService &&
!controller.GetVcModalService;
const numberOfCardsAvailable = !showPinVc
? filteredSearchData.length
: controller.vcMetadatas.length;
const cardsAvailableText =
numberOfCardsAvailable > 1
? numberOfCardsAvailable + ' ' + t('common:cards')
: numberOfCardsAvailable + ' ' + t('common:card');
const CopilotView = walkthroughable(View);
return (
<View
onLayout={controller.isOnboarding ? () => start() : undefined}
style={{
height: Dimensions.get('screen').height * 0.78,
}}>
<Column fill style={{display: props.isVisible ? 'flex' : 'none'}}>
{controller.isRequestSuccessful && (
<BannerNotification
type={BannerStatusType.SUCCESS}
message={t('downloadingYourCard')}
onClosePress={() => {
controller.RESET_STORE_VC_ITEM_STATUS();
clearIndividualId();
}}
key={'downloadingVcPopup'}
testId={'downloadingVcPopup'}
/>
)}
<Column fill pY={2} pX={8}>
{vcMetadataOrderedByPinStatus.length > 0 && (
<React.Fragment>
<Column
scroll
margin="0 0 20 0"
padding="0 0 100 0"
backgroundColor={Theme.Colors.lightGreyBackgroundColor}
refreshControl={
<RefreshControl
refreshing={controller.isRefreshingVcs}
onRefresh={controller.REFRESH}
/>
}>
<Row style={Theme.SearchBarStyles.vcSearchBarContainer}>
<SearchBar
isVcSearch
searchIconTestID="searchIssuerIcon"
searchBarTestID="issuerSearchBar"
search={search}
placeholder={t('searchByName')}
onFocus={onFocusSearch}
onChangeText={filterVcs}
onLayout={() => filterVcs('')}
/>
{clearSearchIcon && (
<Pressable onPress={clearSearchText}>
<Icon
testID="clearingIssuerSearchIcon"
name="circle-with-cross"
type="entypo"
size={15}
color={Theme.Colors.DetailsLabel}
/>
</Pressable>
)}
</Row>
<Row pY={11} pX={8}>
{numberOfCardsAvailable > 0 && (
<Text style={{fontFamily: 'Inter_500Medium'}}>
{cardsAvailableText}
</Text>
)}
</Row>
{showPinVc &&
vcMetadataOrderedByPinStatus.map((vcMetadata, index) => {
return index === 0 || controller.isInitialDownloading ? (
<CopilotStep
text={t('copilot:cardMessage')}
order={6}
name={t('copilot:cardTitle')}>
<CopilotView
onLayout={
controller.isInitialDownloading
? () => {
start();
goToNth(6);
}
: undefined
}>
<VcItemContainer
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
isDownloading={controller.inProgressVcDownloads?.has(
vcMetadata.getVcKey(),
)}
isPinned={vcMetadata.isPinned}
/>
</CopilotView>
</CopilotStep>
) : (
<VcItemContainer
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
isDownloading={controller.inProgressVcDownloads?.has(
vcMetadata.getVcKey(),
)}
isPinned={vcMetadata.isPinned}
/>
);
})}
{filteredSearchData.length > 0 && !showPinVc
? filteredSearchData.map(vcMetadataObj => {
const [vcKey, vcMetadata] =
Object.entries(vcMetadataObj)[0];
return (
<VcItemContainer
key={vcKey}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
isDownloading={controller.inProgressVcDownloads?.has(
vcKey,
)}
/>
);
})
: filteredSearchData.length === 0 &&
search &&
!showPinVc && (
<Column
fill
style={{
justifyContent: 'center',
alignItems: 'center',
paddingTop: 170,
}}>
<Text
style={{
fontWeight: 'bold',
textAlign: 'center',
fontSize: 18,
fontFamily: 'Inter_600SemiBold',
}}>
{t('noCardsTitle')}
</Text>
<Text
style={{
textAlign: 'center',
lineHeight: 17,
paddingTop: 10,
fontSize: 14,
fontFamily: 'Inter_400Regular',
}}>
{t('noCardsDescription')}
</Text>
</Column>
)}
</Column>
</React.Fragment>
)}
{controller.vcMetadatas.length === 0 && (
<React.Fragment>
<Column
scroll
fill
style={Theme.Styles.homeScreenContainer}
refreshControl={
<RefreshControl
refreshing={controller.isRefreshingVcs}
onRefresh={controller.REFRESH}
/>
}>
{SvgImage.DigitalIdentity()}
<Text
testID="bringYourDigitalID"
style={{paddingTop: 3}}
align="center"
weight="bold"
margin="33 0 6 0"
lineHeight={1}>
{t('bringYourDigitalID')}
</Text>
<Text
style={{
...Theme.TextStyles.bold,
paddingTop: 3,
}}
color={Theme.Colors.textLabel}
align="center"
margin="0 12 30 12">
{t('generateVcFABDescription')}
</Text>
</Column>
</React.Fragment>
)}
</Column>
</Column>
{controller.AddVcModalService && (
<AddVcModal service={controller.AddVcModalService} onPress={getId} />
)}
{controller.GetVcModalService && (
<GetVcModal service={controller.GetVcModalService} />
)}
<MessageOverlay
testID="keyStoreNotExists"
isVisible={controller.showHardwareKeystoreNotExistsAlert}
title={t('errors.keystoreNotExists.title')}
message={t('errors.keystoreNotExists.message')}
onButtonPress={controller.ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS}
buttonText={t('errors.keystoreNotExists.riskOkayText')}
minHeight={'auto'}>
<Row>
<Button
testID="ok"
type="gradient"
title={t('errors.keystoreNotExists.riskOkayText')}
onPress={controller.ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS}
margin={[0, 8, 0, 0]}
/>
</Row>
</MessageOverlay>
<MessageOverlay
isVisible={controller.isBindingError}
title={controller.walletBindingError}
onButtonPress={controller.DISMISS}
/>
<MessageOverlay
isVisible={controller.isTampered}
title={t('errors.vcIsTampered.title')}
message={t('errors.vcIsTampered.message')}
onButtonPress={controller.REMOVE_TAMPERED_VCS}
buttonText={t('common:ok')}
minHeight={'auto'}
/>
<MessageOverlay
isVisible={isDownloadFailedVcs}
title={t('errors.downloadLimitExpires.title')}
message={downloadFailedVcsErrorMessage}
onButtonPress={controller.DELETE_VC}
buttonText={t('common:ok')}
minHeight={'auto'}
/>
{isVerificationFailed && (
<Error
testID="verificationError"
isVisible={isVerificationFailed}
isModal={true}
alignActionsOnEnd
title={t('errors.verificationFailed.title')}
message={verificationErrorMessage}
image={SvgImage.PermissionDenied()}
showClose={false}
primaryButtonText="goBack"
primaryButtonEvent={controller.RESET_VERIFY_ERROR}
primaryButtonTestID="goBack"
customStyles={{marginTop: '30%'}}
/>
)}
{controller.isNetworkOff && (
<Error
testID="networkOffError"
isVisible={controller.isNetworkOff}
isModal
title={t('errors.noInternetConnection.title')}
message={t('errors.noInternetConnection.message')}
onDismiss={controller.DISMISS}
image={SvgImage.NoInternetConnection()}
showClose
primaryButtonText="tryAgain"
primaryButtonEvent={controller.TRY_AGAIN}
primaryButtonTestID="tryAgain"
/>
)}
</View>
);
};