diff --git a/.env b/.env index 5a610e11..d9036908 100644 --- a/.env +++ b/.env @@ -18,3 +18,6 @@ CREDENTIAL_REGISTRY_EDIT=true #supported languages( en, fil, ar, hi, kn, ta) APPLICATION_LANGUAGE=en + +#Toggle for openID for VC +ENABLE_OPENID_FOR_VC=false diff --git a/android/app/build.gradle b/android/app/build.gradle index a4d620c6..48608aa1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -121,7 +121,8 @@ android { manifestPlaceholders = [ APP_NAME: APP_NAME_RELEASE, - GOOGLE_NEARBY_MESSAGES_API_KEY: "${properties.getProperty('GOOGLE_NEARBY_MESSAGES_API_KEY')}" + GOOGLE_NEARBY_MESSAGES_API_KEY: "${properties.getProperty('GOOGLE_NEARBY_MESSAGES_API_KEY')}", + appAuthRedirectScheme: 'io.mosip.residentapp.inji' ] } splits { diff --git a/assets/digit-icon.png b/assets/digit-icon.png new file mode 100644 index 00000000..8c1c1d09 Binary files /dev/null and b/assets/digit-icon.png differ diff --git a/assets/no-internet-connection.png b/assets/no-internet-connection.png new file mode 100644 index 00000000..b6fe52e6 Binary files /dev/null and b/assets/no-internet-connection.png differ diff --git a/assets/something-went-wrong.png b/assets/something-went-wrong.png new file mode 100644 index 00000000..52d8c346 Binary files /dev/null and b/assets/something-went-wrong.png differ diff --git a/components/KebabPopUp.tsx b/components/KebabPopUp.tsx index 03b00a24..54d5275b 100644 --- a/components/KebabPopUp.tsx +++ b/components/KebabPopUp.tsx @@ -5,7 +5,7 @@ import {WalletBinding} from '../screens/Home/MyVcs/WalletBinding'; import {Pressable, View} from 'react-native'; import {useKebabPopUp} from './KebabPopUpController'; import {ActorRefFrom} from 'xstate'; -import {vcItemMachine} from '../machines/vcItem'; +import {ExistingMosipVCItemMachine} from '../machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine'; import {useTranslation} from 'react-i18next'; import {HistoryTab} from '../screens/Home/MyVcs/HistoryTab'; import {RemoveVcWarningOverlay} from '../screens/Home/MyVcs/RemoveVcWarningOverlay'; @@ -96,5 +96,5 @@ export interface KebabPopUpProps { vcMetadata: VCMetadata; isVisible: boolean; onDismiss: () => void; - service: ActorRefFrom; + service: ActorRefFrom; } diff --git a/components/KebabPopUpController.tsx b/components/KebabPopUpController.tsx index 030699ad..010098ca 100644 --- a/components/KebabPopUpController.tsx +++ b/components/KebabPopUpController.tsx @@ -1,5 +1,5 @@ -import { useSelector } from '@xstate/react'; -import { ActorRefFrom } from 'xstate'; +import {useSelector} from '@xstate/react'; +import {ActorRefFrom} from 'xstate'; import { selectKebabPopUpWalletBindingInProgress, selectKebabPopUp, @@ -11,49 +11,60 @@ import { selectOtpError, selectShowWalletBindingError, selectWalletBindingError, - VcItemEvents, - vcItemMachine, + ExistingMosipVCItemEvents, + ExistingMosipVCItemMachine, selectShowActivities, -} from '../machines/vcItem'; -import { selectActivities } from '../machines/activityLog'; -import { GlobalContext } from '../shared/GlobalContext'; -import { useContext } from 'react'; -import { VCMetadata } from '../shared/VCMetadata'; +} from '../machines/VCItemMachine/ExistingMosipVCItem/ExistingMosipVCItemMachine'; +import {selectActivities} from '../machines/activityLog'; +import {GlobalContext} from '../shared/GlobalContext'; +import {useContext} from 'react'; +import {VCMetadata} from '../shared/VCMetadata'; +import { + EsignetMosipVCItemEvents, + EsignetMosipVCItemMachine, +} from '../machines/VCItemMachine/EsignetMosipVCItem/EsignetMosipVCItemMachine'; +import {isVCFromOpenId4VCI} from '../shared/openId4VCI/Utils'; export function useKebabPopUp(props) { - const service = props.service as ActorRefFrom; - const PIN_CARD = () => service.send(VcItemEvents.PIN_CARD()); - const KEBAB_POPUP = () => service.send(VcItemEvents.KEBAB_POPUP()); + const service = props.service as + | ActorRefFrom + | ActorRefFrom; + const vcEvents = + props.vcKey !== undefined && isVCFromOpenId4VCI(props.vcMetadata) + ? EsignetMosipVCItemEvents + : ExistingMosipVCItemEvents; + const PIN_CARD = () => service.send(vcEvents.PIN_CARD()); + const KEBAB_POPUP = () => service.send(vcEvents.KEBAB_POPUP()); const ADD_WALLET_BINDING_ID = () => - service.send(VcItemEvents.ADD_WALLET_BINDING_ID()); - const CONFIRM = () => service.send(VcItemEvents.CONFIRM()); + service.send(vcEvents.ADD_WALLET_BINDING_ID()); + const CONFIRM = () => service.send(vcEvents.CONFIRM()); const REMOVE = (vcMetadata: VCMetadata) => - service.send(VcItemEvents.REMOVE(vcMetadata)); - const DISMISS = () => service.send(VcItemEvents.DISMISS()); - const CANCEL = () => service.send(VcItemEvents.CANCEL()); - const SHOW_ACTIVITY = () => service.send(VcItemEvents.SHOW_ACTIVITY()); - const INPUT_OTP = (otp: string) => service.send(VcItemEvents.INPUT_OTP(otp)); + service.send(vcEvents.REMOVE(vcMetadata)); + const DISMISS = () => service.send(vcEvents.DISMISS()); + const CANCEL = () => service.send(vcEvents.CANCEL()); + const SHOW_ACTIVITY = () => service.send(vcEvents.SHOW_ACTIVITY()); + const INPUT_OTP = (otp: string) => service.send(vcEvents.INPUT_OTP(otp)); const isPinned = useSelector(service, selectIsPinned); const isBindingWarning = useSelector(service, selectKebabPopUpBindingWarning); const isRemoveWalletWarning = useSelector(service, selectRemoveWalletWarning); const isAcceptingOtpInput = useSelector( service, - selectKebabPopUpAcceptingBindingOtp + selectKebabPopUpAcceptingBindingOtp, ); const isWalletBindingError = useSelector( service, - selectShowWalletBindingError + selectShowWalletBindingError, ); const otpError = useSelector(service, selectOtpError); const walletBindingError = useSelector(service, selectWalletBindingError); const WalletBindingInProgress = useSelector( service, - selectKebabPopUpWalletBindingInProgress + selectKebabPopUpWalletBindingInProgress, ); const emptyWalletBindingId = useSelector(service, selectEmptyWalletBindingId); const isKebabPopUp = useSelector(service, selectKebabPopUp); const isShowActivities = useSelector(service, selectShowActivities); - const { appService } = useContext(GlobalContext); + const {appService} = useContext(GlobalContext); const activityLogService = appService.children.get('activityLog'); return { diff --git a/components/VC/EsignetMosipVCItem/EsignetMosipVCItem.tsx b/components/VC/EsignetMosipVCItem/EsignetMosipVCItem.tsx new file mode 100644 index 00000000..016b8712 --- /dev/null +++ b/components/VC/EsignetMosipVCItem/EsignetMosipVCItem.tsx @@ -0,0 +1,105 @@ +import React, {useContext, useRef} from 'react'; +import {useInterpret, useSelector} from '@xstate/react'; +import {Pressable} from 'react-native'; +import {ActorRefFrom} from 'xstate'; +import {GlobalContext} from '../../../shared/GlobalContext'; +import {logState} from '../../../machines/app'; +import {Theme} from '../../ui/styleUtils'; +import {Row} from '../../ui'; +import {KebabPopUp} from '../../KebabPopUp'; +import {VCMetadata} from '../../../shared/VCMetadata'; +import {EsignetMosipVCItemContent} from './EsignetMosipVCItemContent'; +import {EsignetMosipVCActivationStatus} from './EsignetMosipVCItemActivationStatus'; +import { + EsignetMosipVCItemEvents, + EsignetMosipVCItemMachine, + createEsignetMosipVCItemMachine, + selectContext, + selectGeneratedOn, + selectKebabPopUp, + selectVerifiableCredentials, +} from '../../../machines/VCItemMachine/EsignetMosipVCItem/EsignetMosipVCItemMachine'; + +export const EsignetMosipVCItem: React.FC = props => { + const {appService} = useContext(GlobalContext); + const machine = useRef( + createEsignetMosipVCItemMachine( + appService.getSnapshot().context.serviceRefs, + props.vcMetadata, + ), + ); + + const service = useInterpret(machine.current, {devTools: __DEV__}); + service.subscribe(logState); + const context = useSelector(service, selectContext); + + const isKebabPopUp = useSelector(service, selectKebabPopUp); + const DISMISS = () => service.send(EsignetMosipVCItemEvents.DISMISS()); + const KEBAB_POPUP = () => + service.send(EsignetMosipVCItemEvents.KEBAB_POPUP()); + + const credentials = useSelector(service, selectVerifiableCredentials); + const generatedOn = useSelector(service, selectGeneratedOn); + + return ( + + props.onPress(service)} + disabled={!credentials} + style={ + props.selected + ? Theme.Styles.selectedBindedVc + : Theme.Styles.closeCardBgContainer + }> + props.onPress(service)} + /> + {props.isSharingVc ? null : ( + + {props.activeTab !== 'receivedVcsTab' && + props.activeTab != 'sharingVcScreen' && ( + + )} + + + + + )} + + + ); +}; + +export interface EsignetMosipVCItemProps { + vcMetadata: VCMetadata; + margin?: string; + selectable?: boolean; + selected?: boolean; + showOnlyBindedVc?: boolean; + onPress?: (vcRef?: ActorRefFrom) => void; + onShow?: (vcRef?: ActorRefFrom) => void; + activeTab?: string; + iconName?: string; + iconType?: string; + isSharingVc?: boolean; +} diff --git a/components/VC/EsignetMosipVCItem/EsignetMosipVCItemActivationStatus.tsx b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemActivationStatus.tsx new file mode 100644 index 00000000..cb12afc6 --- /dev/null +++ b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemActivationStatus.tsx @@ -0,0 +1,130 @@ +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {Dimensions} from 'react-native'; +import {Icon} from 'react-native-elements'; +import {Theme} from '../../ui/styleUtils'; +import {Row, Text} from '../../ui'; +import {VerifiableCredential} from './vc'; + +const WalletUnverifiedIcon: React.FC = () => { + return ( + + ); +}; + +const WalletVerifiedIcon: React.FC = () => { + return ( + + ); +}; + +const WalletUnverifiedActivationDetails: React.FC< + WalletUnVerifiedDetailsProps +> = props => { + const {t} = useTranslation('VcDetails'); + return ( + + + {props.verifiableCredential && } + + {t('profileAuthenticated')} + + + + ); +}; + +const WalletVerifiedActivationDetails: React.FC< + WalletVerifiedDetailsProps +> = props => { + const {t} = useTranslation('VcDetails'); + return ( + + + + + {t('profileAuthenticated')} + + + + ); +}; + +export const EsignetMosipVCActivationStatus: React.FC< + EsignetMosipVCActivationStatusProps +> = props => { + return ( + + {props.emptyWalletBindingId ? ( + + ) : ( + + )} + + ); +}; + +export interface EsignetMosipVCActivationStatusProps { + showOnlyBindedVc: boolean; + verifiableCredential: VerifiableCredential; + emptyWalletBindingId: boolean; +} + +interface WalletVerifiedDetailsProps { + showOnlyBindedVc: boolean; + verifiableCredential: VerifiableCredential; +} + +interface WalletUnVerifiedDetailsProps { + verifiableCredential: VerifiableCredential; +} diff --git a/components/VC/EsignetMosipVCItem/EsignetMosipVCItemContent.tsx b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemContent.tsx new file mode 100644 index 00000000..e08d86c1 --- /dev/null +++ b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemContent.tsx @@ -0,0 +1,201 @@ +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {Image, ImageBackground} from 'react-native'; +import {CheckBox, Icon} from 'react-native-elements'; +import {Column, Row, Text} from '../../ui'; +import {Theme} from '../../ui/styleUtils'; +import VerifiedIcon from '../../VerifiedIcon'; +import {getLocalizedField} from '../../../i18n'; +import {Credential, VerifiableCredential} from './vc'; +import testIDProps from '../../../shared/commonUtil'; + +const getDetails = (arg1: string, arg2: string, credential: Credential) => { + if (arg1 === 'Status') { + return ( + + + {arg1} + + + + {!credential ? '' : arg2} + + {!credential ? null : } + + + ); + } else { + return ( + + + {arg1} + + + {!credential ? '' : arg2} + + + ); + } +}; + +export const EsignetMosipVCItemContent: React.FC< + EsignetMosipVCItemContentProps +> = props => { + const fullName = !props.credential + ? '' + : getLocalizedField( + props.credential?.credential.credentialSubject?.fullName, + ); + const {t} = useTranslation('VcDetails'); + const isvalid = !props.credential ? '' : t('valid'); + const selectableOrCheck = props.selectable ? ( + } + uncheckedIcon={} + onPress={() => props.onPress()} + /> + ) : null; + + return ( + + + + + + {props.iconName && ( + + )} + + + {getDetails( + t('fullName'), + fullName, + props.credential?.credential, + )} + + + + {t('idType')} + + + {t('nationalCard')} + + + + + + {props.credential ? selectableOrCheck : null} + + + + + {!props.credential + ? getDetails(t('id'), 'newid', props.credential?.credential) + : null} + {getDetails( + t('generatedOn'), + props.generatedOn, + props.credential?.credential, + )} + + + {props.credential + ? getDetails(t('status'), isvalid, props.credential?.credential) + : null} + + + + + + + + ); +}; + +interface EsignetMosipVCItemContentProps { + context: any; + credential: VerifiableCredential; + generatedOn: string; + selectable: boolean; + selected: boolean; + iconName?: string; + iconType?: string; + service: any; + onPress?: () => void; +} diff --git a/components/VC/EsignetMosipVCItem/EsignetMosipVCItemDetails.tsx b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemDetails.tsx new file mode 100644 index 00000000..b62e089b --- /dev/null +++ b/components/VC/EsignetMosipVCItem/EsignetMosipVCItemDetails.tsx @@ -0,0 +1,401 @@ +import React from 'react'; +import {useTranslation} from 'react-i18next'; +import {Button, Column, Row, Text} from '../../ui'; +import {Image, ImageBackground, View} from 'react-native'; +import {Theme} from '../../ui/styleUtils'; +import {QrCodeOverlay} from '../../QrCodeOverlay'; +import {getLocalizedField} from '../../../i18n'; +import VerifiedIcon from '../../VerifiedIcon'; +import {CREDENTIAL_REGISTRY_EDIT} from 'react-native-dotenv'; +import {TextItem} from '../../ui/TextItem'; +import {format, formatDistanceToNow, parse} from 'date-fns'; +import DateFnsLocale from 'date-fns/locale'; +import {Icon} from 'react-native-elements'; +import {WalletBindingResponse} from '../../../shared/cryptoutil/cryptoUtil'; +import { + CredentialSubject, + VCSharingReason, + VcIdType, + VerifiableCredential, + VerifiablePresentation, +} from './vc'; + +export const EsignetMosipVCItemDetails: React.FC< + EsignetMosipVCItemDetailsProps +> = props => { + const {t, i18n} = useTranslation('VcDetails'); + + if (props.vc?.verifiableCredential == null) { + return Loading details...; + } + + return ( + + + + + + + + + + + + + + + {t('fullName')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credential.credentialSubject + .fullName, + )} + + + + + + + {t('idType')} + + + {t('nationalCard')} + + + {props.vc?.verifiableCredential.credential.credentialSubject + .VID ? ( + + + {t('vid')} + + + { + props.vc?.verifiableCredential.credential + .credentialSubject.VID + } + + + ) : null} + + + {t('dateOfBirth')} + + + {format( + parse( + getLocalizedField( + props.vc?.verifiableCredential.credential + .credentialSubject.dateOfBirth, + ), + 'yyyy/MM/dd', + new Date(), + ), + 'yyy/MM/dd', + )} + + + + + + + {t('gender')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credential + .credentialSubject.gender, + )} + + + + + {t('generatedOn')} + + + {new Date(props.vc?.generatedOn).toLocaleDateString()} + + + + + {t('status')} + + + + {t('valid')} + + {props.vc?.isVerified && } + + + + + {t('phoneNumber')} + + + {getLocalizedField( + props.vc?.verifiableCredential.credential + .credentialSubject.phone, + )} + + + + + + + + + + + {t('email')} + + + 25 + ? {flex: 1} + : {flex: 0} + } + weight="semibold" + size="smaller" + color={Theme.Colors.Details}> + {getLocalizedField( + props.vc?.verifiableCredential.credential.credentialSubject + .email, + )} + + + + + + + {t('address')} + + + + {getFullAddress( + props.vc?.verifiableCredential.credential.credentialSubject, + )} + + + + {CREDENTIAL_REGISTRY_EDIT === 'true' && ( + + + {t('credentialRegistry')} + + + {props.vc?.credentialRegistry} + + + )} + + + + {props.vc?.reason?.length > 0 && ( + + {t('reasonForSharing')} + + )} + + {props.vc?.reason?.map((reason, index) => ( + + ))} + + {props.activeTab !== 1 ? ( + props.isBindingPending ? ( + + + + + {t('offlineAuthDisabledHeader')} + + + + {t('offlineAuthDisabledMessage')} + + +