Inji-344: Refactoring VC Key (#798)

* feat(inji-344): Use VC Key class instead of separate functions for managing vc key

* feat(inji-344): Use properties from VcKey Class instead of reading from vckey string

* feat(inji-344): Rename vcKey to vcMetadata

* feat(inji-344): Use vc's unique id or vckey instead of joined string of vc metadata

* feat(inji-344): Use vc key instead of unique id to avoid confusion. Fix issues reg parsing vc metadata

* feat(inji-344):fix redownloading issue

Co-authored-by: Tilak <tilakpuli15@gmail.com>

* feat(inji-344): Remove vc getting stored on update of pin status

* feat(inji-344): update other vc's pin status to false when any vc is pinned

* feat(inji-344): remove hash ID for UIN

* feat(inji-344): revert google services json

* feat(inji-344): remove mmkv logs added for debugging

* feat(inji-344): fix received vcs not getting displayed on reopen of app

* feat(inji-344): fix id not shown in revoke component

---------

Co-authored-by: Sri Kanth Kola <srikanthsri7447@gmail.com>
This commit is contained in:
Tilak Puli
2023-09-20 16:51:59 +05:30
committed by GitHub
parent 43efe2a2fc
commit ec9fd9f6d9
61 changed files with 1006 additions and 968 deletions

View File

@@ -64,7 +64,7 @@ const AppLoadingWrapper: React.FC = () => {
const isReadError = useSelector(appService, selectIsReadError);
const isKeyInvalidateError = useSelector(
appService,
selectIsKeyInvalidateError
selectIsKeyInvalidateError,
);
const controller = useApp();
const {t} = useTranslation('WelcomeScreen');
@@ -102,7 +102,7 @@ const AppInitialization: React.FC = () => {
if (isCustomSecureKeystore()) {
SecureKeystore.updatePopup(
t('biometricPopup.title'),
t('biometricPopup.description')
t('biometricPopup.description'),
);
}
}, [i18n.language]);

View File

@@ -5,7 +5,7 @@ import { Text, Column, Row, Button } from './ui';
import {Theme} from './ui/styleUtils';
import {useTranslation} from 'react-i18next';
export const EditableListItem: React.FC<EditableListItemProps> = (props) => {
export const EditableListItem: React.FC<EditableListItemProps> = props => {
const {t} = useTranslation('common');
const [isEditing, setIsEditing] = useState(false);
const [items, setItems] = useState(props.items);
@@ -18,7 +18,7 @@ export const EditableListItem: React.FC<EditableListItemProps> = (props) => {
}, [props.response]);
function updateItems(label: string, value: string) {
const updatedItems = items.map((item) => {
const updatedItems = items.map(item => {
if (item.label === label) {
return {...item, value: value};
}
@@ -61,7 +61,7 @@ export const EditableListItem: React.FC<EditableListItemProps> = (props) => {
<Input
autoFocus
value={items[index].value}
onChangeText={(value) => updateItems(item.label, value)}
onChangeText={value => updateItems(item.label, value)}
selectionColor={Theme.Colors.Cursor}
inputStyle={{
textAlign: I18nManager.isRTL ? 'right' : 'left',

View File

@@ -1,4 +1,3 @@
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';
@@ -11,6 +10,7 @@ import {useTranslation} from 'react-i18next';
import {HistoryTab} from '../screens/Home/MyVcs/HistoryTab';
import {RemoveVcWarningOverlay} from '../screens/Home/MyVcs/RemoveVcWarningOverlay';
import {ScrollView} from 'react-native-gesture-handler';
import {VCMetadata} from '../shared/VCMetadata';
import testIDProps from '../shared/commonUtil';
export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
@@ -46,9 +46,7 @@ export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
<ListItem.Title>
<Pressable onPress={controller.PIN_CARD}>
<Text size="small" weight="bold">
{props.vcKey.split(':')[4] == 'true'
? t('unPinCard')
: t('pinCard')}
{props.vcMetadata.isPinned ? t('unPinCard') : t('pinCard')}
</Text>
</Pressable>
</ListItem.Title>
@@ -65,13 +63,13 @@ export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
testID="viewActivityLog"
service={props.service}
label={t('viewActivityLog')}
vcKey={props.vcKey}
vcMetadata={props.vcMetadata}
/>
<ListItem testID="removeFromWallet" bottomDivider>
<ListItem.Content>
<ListItem.Title>
<Pressable onPress={() => controller.REMOVE(props.vcKey)}>
<Pressable onPress={() => controller.REMOVE(props.vcMetadata)}>
<Text size="small" weight="bold">
{t('removeFromWallet')}
</Text>
@@ -94,7 +92,7 @@ export const KebabPopUp: React.FC<KebabPopUpProps> = props => {
export interface KebabPopUpProps {
iconName: string;
iconType?: string;
vcKey: string;
vcMetadata: VCMetadata;
isVisible: boolean;
onDismiss: () => void;
service: ActorRefFrom<typeof vcItemMachine>;

View File

@@ -18,6 +18,7 @@ import {
import { selectActivities } from '../machines/activityLog';
import { GlobalContext } from '../shared/GlobalContext';
import { useContext } from 'react';
import { VCMetadata } from '../shared/VCMetadata';
export function useKebabPopUp(props) {
const service = props.service as ActorRefFrom<typeof vcItemMachine>;
@@ -26,7 +27,8 @@ export function useKebabPopUp(props) {
const ADD_WALLET_BINDING_ID = () =>
service.send(VcItemEvents.ADD_WALLET_BINDING_ID());
const CONFIRM = () => service.send(VcItemEvents.CONFIRM());
const REMOVE = (vcKey: string) => service.send(VcItemEvents.REMOVE(vcKey));
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());

View File

@@ -1,4 +1,4 @@
import React, {useContext, useRef} from 'react';
import React, {useContext, useEffect, useRef} from 'react';
import {useInterpret, useSelector} from '@xstate/react';
import {Pressable} from 'react-native';
import {ActorRefFrom} from 'xstate';
@@ -22,18 +22,23 @@ import {VcItemActivationStatus} from './VcItemActivationStatus';
import {Row} from './ui';
import {KebabPopUp} from './KebabPopUp';
import {logState} from '../machines/app';
import {VCMetadata} from '../shared/VCMetadata';
export const VcItem: React.FC<VcItemProps> = props => {
const {appService} = useContext(GlobalContext);
const machine = useRef(
createVcItemMachine(
appService.getSnapshot().context.serviceRefs,
props.vcKey,
props.vcMetadata,
),
);
const service = useInterpret(machine.current, {devTools: __DEV__});
useEffect(() => {
service.subscribe(logState);
}, [service]);
const context = useSelector(service, selectContext);
const verifiableCredential = useSelector(service, selectVerifiableCredential);
const emptyWalletBindingId = useSelector(service, selectEmptyWalletBindingId);
@@ -82,7 +87,7 @@ export const VcItem: React.FC<VcItemProps> = props => {
)}
<Pressable onPress={KEBAB_POPUP}>
<KebabPopUp
vcKey={props.vcKey}
vcMetadata={props.vcMetadata}
iconName="dots-three-horizontal"
iconType="entypo"
isVisible={isKebabPopUp}
@@ -104,7 +109,7 @@ export const VcItem: React.FC<VcItemProps> = props => {
};
interface VcItemProps {
vcKey: string;
vcMetadata: VCMetadata;
margin?: string;
selectable?: boolean;
selected?: boolean;

View File

@@ -16,13 +16,14 @@ import { Theme } from './ui/styleUtils';
import { RotatingIcon } from './RotatingIcon';
import { GlobalContext } from '../shared/GlobalContext';
import { getLocalizedField } from '../i18n';
import { VCMetadata } from '../shared/VCMetadata';
export const VidItem: React.FC<VcItemProps> = (props) => {
const { appService } = useContext(GlobalContext);
const machine = useRef(
createVcItemMachine(
appService.getSnapshot().context.serviceRefs,
props.vcKey
props.vcMetadata
)
);
const service = useInterpret(machine.current);
@@ -99,7 +100,7 @@ export const VidItem: React.FC<VcItemProps> = (props) => {
};
interface VcItemProps {
vcKey: string;
vcMetadata: VCMetadata;
margin?: string;
selectable?: boolean;
selected?: boolean;

View File

@@ -8,7 +8,7 @@ import { Text } from './Text';
import {Theme, Spacing} from './styleUtils';
import testIDProps from '../../shared/commonUtil';
export const Button: React.FC<ButtonProps> = (props) => {
export const Button: React.FC<ButtonProps> = props => {
const type = props.type || 'solid' || 'radius' || 'gradient';
const buttonStyle: StyleProp<ViewStyle> = [
props.fill ? Theme.ButtonStyles.fill : null,

View File

@@ -15,7 +15,7 @@ import testIDProps from '../../shared/commonUtil';
function createLayout(
direction: FlexStyle['flexDirection'],
mainAlign?: FlexStyle['justifyContent'],
crossAlign?: FlexStyle['alignItems']
crossAlign?: FlexStyle['alignItems'],
) {
const layoutStyles = StyleSheet.create({
base: {
@@ -28,7 +28,7 @@ function createLayout(
},
});
const Layout: React.FC<LayoutProps> = (props) => {
const Layout: React.FC<LayoutProps> = props => {
const styles: StyleProp<ViewStyle> = [
layoutStyles.base,
props.fill ? layoutStyles.fill : null,
@@ -73,7 +73,7 @@ export const Centered = createLayout('column', 'center', 'center');
export const HorizontallyCentered = createLayout(
'column',
'flex-start',
'center'
'center',
);
interface LayoutProps {

View File

@@ -59,7 +59,7 @@ export function getLanguageCode(code: string) {
export function getVCDetailsForCurrentLanguage(locales) {
const currentLanguage = i18next.language;
const vcDetailsForCurrentLanguage = locales.filter(
(obj) => obj.language === languageCodeMap[currentLanguage]
obj => obj.language === languageCodeMap[currentLanguage],
);
return vcDetailsForCurrentLanguage[0]?.value
? vcDetailsForCurrentLanguage[0].value
@@ -70,13 +70,13 @@ export function getVCDetailsForCurrentLanguage(locales) {
// The response received from the server is three letter language code and the value in the inji code base is two letter language code. Hence the conversion is done.
function getThreeLetterLanguageCode(twoLetterLanguageCode) {
return Object.keys(iso6393To1).find(
(key) => iso6393To1[key] === twoLetterLanguageCode
key => iso6393To1[key] === twoLetterLanguageCode,
);
}
function populateLanguageCodeMap() {
const supportedLanguages = Object.keys(SUPPORTED_LANGUAGES);
supportedLanguages.forEach((languageCode) => {
supportedLanguages.forEach(languageCode => {
let threeLetterLanguageCode = languageCode;
if (isTwoLetterLanguageCode(languageCode)) {

View File

@@ -281,6 +281,7 @@
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXConstants.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EXUpdates.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GNSSharedResources.bundle",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Assets.car",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -762,7 +762,7 @@ SPEC CHECKSUMS:
BVLinearGradient: 916632041121a658c704df89d99f04acb038de0f
CatCrypto: a477899b6be4954e75be4897e732da098cc0a5a8
CrcSwift: f85dea6b41dddb5f98bb3743fd777ce58b77bc2e
DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
EASClient: 950674e1098ebc09c4c2cf064a61e42e84d9d4c6
EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903
EXBarCodeScanner: 8e23fae8d267dbef9f04817833a494200f1fce35
@@ -785,7 +785,7 @@ SPEC CHECKSUMS:
FBLazyVector: f637f31eacba90d4fdeff3fa41608b8f361c173b
FBReactNativeSpec: 0d9a4f4de7ab614c49e98c00aedfd3bfbda33d59
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
GoogleInterchangeUtilities: d5bc4d88d5b661ab72f9d70c58d02ca8c27ad1f7
GoogleNetworkingUtilities: 3edd3a8161347494f2da60ea0deddc8a472d94cb
GoogleSymbolUtilities: 631ee17048aa5e9ab133470d768ea997a5ef9b96
@@ -855,4 +855,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: 01f58b130fa221dabb14b2d82d981ef24dcaba53
COCOAPODS: 1.12.1
COCOAPODS: 1.11.3

View File

@@ -12,15 +12,13 @@ import { MY_VCS_STORE_KEY, ESIGNET_BASE_URL } from '../shared/constants';
import {StoreEvents} from './store';
import {linkTransactionResponse, VC} from '../types/vc';
import {request} from '../shared/request';
import {
getJwt,
isCustomSecureKeystore,
} from '../shared/cryptoutil/cryptoUtil';
import {getJwt, isCustomSecureKeystore} from '../shared/cryptoutil/cryptoUtil';
import {
getBindingCertificateConstant,
getPrivateKey,
} from '../shared/keystore/SecureKeystore';
import i18n from '../i18n';
import {parseMetadatas, VCMetadata} from '../shared/VCMetadata';
import {getEndData, sendEndEvent} from '../shared/telemetry/TelemetryUtils';
const model = createModel(
@@ -28,7 +26,7 @@ const model = createModel(
serviceRefs: {} as AppServices,
selectedVc: {} as VC,
linkCode: '',
myVcs: [] as string[],
myVcs: [] as VCMetadata[],
thumbprint: '',
linkTransactionResponse: {} as linkTransactionResponse,
authFactors: [],
@@ -65,7 +63,7 @@ const model = createModel(
FACE_INVALID: () => ({}),
RETRY_VERIFICATION: () => ({}),
},
}
},
);
export const QrLoginEvents = model.events;
@@ -235,7 +233,7 @@ export const qrLoginMachine =
},
done: {
type: 'final',
data: (context) => context,
data: context => context,
},
},
},
@@ -247,22 +245,24 @@ export const qrLoginMachine =
linkCode: (context, event) => event.value,
}),
// TODO: loaded VCMetadatas are not used anywhere. remove?
loadMyVcs: send(StoreEvents.GET(MY_VCS_STORE_KEY), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
setMyVcs: model.assign({
myVcs: (_context, event) => (event.response || []) as string[],
myVcs: (_context, event) =>
parseMetadatas((event.response || []) as object[]),
}),
loadThumbprint: send(
(context) =>
context =>
StoreEvents.GET(
getBindingCertificateConstant(
context.selectedVc.walletBindingResponse?.walletBindingId
)
context.selectedVc.walletBindingResponse?.walletBindingId,
),
{ to: (context) => context.serviceRefs.store }
),
{to: context => context.serviceRefs.store},
),
setThumbprint: assign({
thumbprint: (_context, event) => {
@@ -289,29 +289,29 @@ export const qrLoginMachine =
}),
expandLinkTransResp: assign({
authFactors: (context) => context.linkTransactionResponse.authFactors,
authFactors: context => context.linkTransactionResponse.authFactors,
authorizeScopes: (context) =>
authorizeScopes: context =>
context.linkTransactionResponse.authorizeScopes,
clientName: (context) => context.linkTransactionResponse.clientName,
clientName: context => context.linkTransactionResponse.clientName,
configs: (context) => context.linkTransactionResponse.configs,
configs: context => context.linkTransactionResponse.configs,
essentialClaims: (context) =>
essentialClaims: context =>
context.linkTransactionResponse.essentialClaims,
linkTransactionId: (context) =>
linkTransactionId: context =>
context.linkTransactionResponse.linkTransactionId,
logoUrl: (context) => context.linkTransactionResponse.logoUrl,
logoUrl: context => context.linkTransactionResponse.logoUrl,
voluntaryClaims: (context) =>
voluntaryClaims: context =>
context.linkTransactionResponse.voluntaryClaims,
}),
setClaims: (context) => {
context.voluntaryClaims.map((claim) => {
setClaims: context => {
context.voluntaryClaims.map(claim => {
context.isSharing[claim] = false;
});
},
@@ -331,7 +331,7 @@ export const qrLoginMachine =
} else {
context.selectedVoluntaryClaims =
context.selectedVoluntaryClaims.filter(
(eachClaim) => eachClaim !== event.claim
eachClaim => eachClaim !== event.claim,
);
}
return {...context.isSharing};
@@ -342,7 +342,7 @@ export const qrLoginMachine =
}),
},
services: {
linkTransaction: async (context) => {
linkTransaction: async context => {
const response = await request(
'POST',
'/v1/esignet/linked-authorization/link-transaction',
@@ -352,17 +352,17 @@ export const qrLoginMachine =
linkCode: context.linkCode,
},
},
ESIGNET_BASE_URL
ESIGNET_BASE_URL,
);
return response.response;
},
sendAuthenticate: async (context) => {
sendAuthenticate: async context => {
let privateKey;
if (!isCustomSecureKeystore()) {
privateKey = await getPrivateKey(
context.selectedVc.walletBindingResponse?.walletBindingId
context.selectedVc.walletBindingResponse?.walletBindingId,
);
}
@@ -370,7 +370,7 @@ export const qrLoginMachine =
var jwt = await getJwt(
privateKey,
context.selectedVc.id,
context.thumbprint
context.thumbprint,
);
const response = await request(
@@ -390,23 +390,23 @@ export const qrLoginMachine =
],
},
},
ESIGNET_BASE_URL
ESIGNET_BASE_URL,
);
return response.response.linkedTransactionId;
},
sendConsent: async (context) => {
sendConsent: async context => {
let privateKey;
if (!isCustomSecureKeystore()) {
privateKey = await getPrivateKey(
context.selectedVc.walletBindingResponse?.walletBindingId
context.selectedVc.walletBindingResponse?.walletBindingId,
);
}
const jwt = await getJwt(
privateKey,
context.selectedVc.id,
context.thumbprint
context.thumbprint,
);
const response = await request(
@@ -426,7 +426,7 @@ export const qrLoginMachine =
],
},
},
ESIGNET_BASE_URL
ESIGNET_BASE_URL,
);
var linkedTrnId = response.response.linkedTransactionId;
@@ -438,17 +438,17 @@ export const qrLoginMachine =
request: {
linkedTransactionId: linkedTrnId,
acceptedClaims: context.essentialClaims.concat(
context.selectedVoluntaryClaims
context.selectedVoluntaryClaims,
),
permittedAuthorizeScopes: context.authorizeScopes,
},
},
ESIGNET_BASE_URL
ESIGNET_BASE_URL,
);
console.log(resp.response.linkedTransactionId);
},
},
}
},
);
export function createQrLoginMachine(serviceRefs: AppServices) {

View File

@@ -55,7 +55,7 @@ const model = createModel(
STORE_RESPONSE: (response: unknown) => ({response}),
RESET_KEY_INVALIDATE_ERROR_DISMISS: () => ({}),
},
}
},
);
export const APP_EVENTS = model.events;
@@ -208,9 +208,9 @@ export const appMachine = model.createMachine(
{
actions: {
forwardToServices: pure((context, event) =>
Object.values(context.serviceRefs).map((serviceRef) =>
send({ ...event, type: `APP_${event.type}` }, { to: serviceRef })
)
Object.values(context.serviceRefs).map(serviceRef =>
send({...event, type: `APP_${event.type}`}, {to: serviceRef}),
),
),
setIsReadError: assign({
isReadError: true,
@@ -237,7 +237,7 @@ export const appMachine = model.createMachine(
isKeyInvalidateError: false,
}),
requestDeviceInfo: respond((context) => ({
requestDeviceInfo: respond(context => ({
type: 'RECEIVE_DEVICE_INFO',
info: {
...context.info,
@@ -246,63 +246,63 @@ export const appMachine = model.createMachine(
})),
spawnStoreActor: assign({
serviceRefs: (context) => ({
serviceRefs: context => ({
...context.serviceRefs,
store: spawn(storeMachine, storeMachine.id),
}),
}),
logStoreEvents: (context) => {
logStoreEvents: context => {
if (__DEV__) {
context.serviceRefs.store.subscribe(logState);
}
},
spawnServiceActors: model.assign({
serviceRefs: (context) => {
serviceRefs: context => {
const serviceRefs = {
...context.serviceRefs,
};
serviceRefs.auth = spawn(
createAuthMachine(serviceRefs),
authMachine.id
authMachine.id,
);
serviceRefs.vc = spawn(createVcMachine(serviceRefs), vcMachine.id);
serviceRefs.settings = spawn(
createSettingsMachine(serviceRefs),
settingsMachine.id
settingsMachine.id,
);
serviceRefs.activityLog = spawn(
createActivityLogMachine(serviceRefs),
activityLogMachine.id
activityLogMachine.id,
);
serviceRefs.scan = spawn(
createScanMachine(serviceRefs),
scanMachine.id
scanMachine.id,
);
if (Platform.OS === 'android') {
serviceRefs.request = spawn(
createRequestMachine(serviceRefs),
requestMachine.id
requestMachine.id,
);
}
serviceRefs.revoke = spawn(
createRevokeMachine(serviceRefs),
revokeVidsMachine.id
revokeVidsMachine.id,
);
return serviceRefs;
},
}),
logServiceEvents: (context) => {
logServiceEvents: context => {
if (__DEV__) {
context.serviceRefs.auth.subscribe(logState);
context.serviceRefs.vc.subscribe(logState);
@@ -329,19 +329,19 @@ export const appMachine = model.createMachine(
loadCredentialRegistryHostFromStorage: send(
StoreEvents.GET(SETTINGS_STORE_KEY),
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
loadEsignetHostFromStorage: send(StoreEvents.GET(SETTINGS_STORE_KEY), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
loadCredentialRegistryInConstants: (_context, event) => {
changeCrendetialRegistry(
!event.response?.credentialRegistry
? MIMOTO_BASE_URL
: event.response?.credentialRegistry
: event.response?.credentialRegistry,
);
},
@@ -349,13 +349,13 @@ export const appMachine = model.createMachine(
changeEsignetUrl(
!event.response?.esignetHostUrl
? ESIGNET_BASE_URL
: event.response?.esignetHostUrl
: event.response?.esignetHostUrl,
);
},
},
services: {
getAppInfo: () => async (callback) => {
getAppInfo: () => async callback => {
const appInfo = {
deviceId: getDeviceId(),
deviceName: await getDeviceName(),
@@ -363,7 +363,7 @@ export const appMachine = model.createMachine(
callback(model.events.APP_INFO_RECEIVED(appInfo));
},
getBackendInfo: () => async (callback) => {
getBackendInfo: () => async callback => {
let backendInfo = {
application: {
name: '',
@@ -380,7 +380,7 @@ export const appMachine = model.createMachine(
}
},
checkFocusState: () => (callback) => {
checkFocusState: () => callback => {
const changeHandler = (newState: AppStateStatus) => {
switch (newState) {
case 'background':
@@ -414,8 +414,8 @@ export const appMachine = model.createMachine(
};
},
checkNetworkState: () => (callback) => {
return NetInfo.addEventListener((state) => {
checkNetworkState: () => callback => {
return NetInfo.addEventListener(state => {
if (state.isConnected) {
callback({type: 'ONLINE', networkType: state.type});
} else {
@@ -424,7 +424,7 @@ export const appMachine = model.createMachine(
});
},
},
}
},
);
interface AppInfo {
@@ -477,7 +477,7 @@ export function logState(state: AnyState) {
}
return value;
},
2
2,
);
console.log(
`[${getDeviceNameSync()}] ${state.machine.id}: ${
@@ -485,7 +485,7 @@ export function logState(state: AnyState) {
} -> ${state.toStrings().pop()}\n${
data.length > 300 ? data.slice(0, 300) + '...' : data
}
`
`,
);
}

View File

@@ -2,22 +2,22 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
internalEvents: {
'xstate.init': {type: 'xstate.init'};
};
'invokeSrcNameMap': {
invokeSrcNameMap: {
checkFocusState: 'done.invoke.app.ready.focus:invocation[0]';
checkNetworkState: 'done.invoke.app.ready.network:invocation[0]';
getAppInfo: 'done.invoke.app.init.info:invocation[0]';
getBackendInfo: 'done.invoke.app.init.devinfo:invocation[0]';
};
'missingImplementations': {
missingImplementations: {
actions: never;
delays: never;
guards: never;
services: never;
};
'eventsCausingActions': {
eventsCausingActions: {
forwardToServices: 'ACTIVE' | 'INACTIVE' | 'OFFLINE' | 'ONLINE';
loadCredentialRegistryHostFromStorage: 'READY';
loadCredentialRegistryInConstants: 'STORE_RESPONSE';
@@ -41,15 +41,15 @@ export interface Typegen0 {
unsetIsReadError: 'READY';
updateKeyInvalidateError: 'ERROR' | 'KEY_INVALIDATE_ERROR';
};
'eventsCausingDelays': {};
'eventsCausingGuards': {};
'eventsCausingServices': {
eventsCausingDelays: {};
eventsCausingGuards: {};
eventsCausingServices: {
checkFocusState: 'BACKEND_INFO_RECEIVED';
checkNetworkState: 'BACKEND_INFO_RECEIVED';
getAppInfo: 'STORE_RESPONSE';
getBackendInfo: 'APP_INFO_RECEIVED';
};
'matchesStates':
matchesStates:
| 'init'
| 'init.credentialRegistry'
| 'init.devinfo'
@@ -76,5 +76,5 @@ export interface Typegen0 {
network?: 'checking' | 'offline' | 'online';
};
};
'tags': never;
tags: never;
}

View File

@@ -28,7 +28,7 @@ const model = createModel(
SELECT: () => ({}),
NEXT: () => ({}),
},
}
},
);
export const AuthEvents = model.events;
@@ -131,15 +131,15 @@ export const authMachine = model.createMachine(
{
actions: {
requestStoredContext: send(StoreEvents.GET('auth'), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
storeContext: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
return StoreEvents.SET('auth', data);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
setContext: model.assign((_, event) => {
@@ -158,7 +158,7 @@ export const authMachine = model.createMachine(
}),
setLanguage: assign({
selectLanguage: (context) => true,
selectLanguage: context => true,
}),
setPasscodeSalt: assign({
@@ -172,7 +172,7 @@ export const authMachine = model.createMachine(
downloadFaceSdkModel: () => () => {
downloadModel();
},
generatePasscodeSalt: () => async (context) => {
generatePasscodeSalt: () => async context => {
const randomBytes = await generateSecureRandom(16);
return binaryToBase64(randomBytes) as string;
},
@@ -181,17 +181,17 @@ export const authMachine = model.createMachine(
guards: {
hasData: (_, event: StoreResponseEvent) => event.response != null,
hasPasscodeSet: (context) => {
hasPasscodeSet: context => {
return context.passcode !== '';
},
hasBiometricSet: (context) => {
hasBiometricSet: context => {
return context.biometrics !== '' && context.passcode !== '';
},
hasLanguageset: (context) => {
hasLanguageset: context => {
return !context.selectLanguage;
},
},
}
},
);
export function createAuthMachine(serviceRefs: AppServices) {

View File

@@ -14,10 +14,7 @@ import {getDeviceNameSync} from 'react-native-device-info';
import {StoreEvents} from '../../store';
import {VC} from '../../../types/vc';
import {AppServices} from '../../../shared/GlobalContext';
import {
RECEIVED_VCS_STORE_KEY,
VC_ITEM_STORE_KEY,
} from '../../../shared/constants';
import {RECEIVED_VCS_STORE_KEY} from '../../../shared/constants';
import {ActivityLogEvents, ActivityLogType} from '../../activityLog';
import {VcEvents} from '../../vc';
import {subscribe} from '../../../shared/openIdBLE/verifierEventHandler';
@@ -25,6 +22,7 @@ import {log} from 'xstate/lib/actions';
import {VerifierDataEvent} from 'react-native-tuvali/src/types/events';
import {BLEError} from '../types';
import Storage from '../../../shared/storage';
import {VCMetadata} from '../../../shared/VCMetadata';
// import { verifyPresentation } from '../shared/vcjs/verifyPresentation';
const {verifier, EventTypes, VerificationStatus} = tuvali;
@@ -67,7 +65,7 @@ const model = createModel(
STORE_ERROR: (error: Error) => ({error}),
RECEIVE_DEVICE_INFO: (info: DeviceInfo) => ({info}),
RECEIVED_VCS_UPDATED: () => ({}),
VC_RESPONSE: (response: unknown) => ({response}),
VC_RESPONSE: (vcMetadatas: VCMetadata[]) => ({vcMetadatas}),
GOTO_SETTINGS: () => ({}),
APP_ACTIVE: () => ({}),
FACE_VALID: () => ({}),
@@ -584,13 +582,14 @@ export const requestMachine =
context =>
StoreEvents.PREPEND(
RECEIVED_VCS_STORE_KEY,
VC_ITEM_STORE_KEY(context.incomingVc),
VCMetadata.fromVC(context.incomingVc),
),
{to: context => context.serviceRefs.store},
),
requestExistingVc: send(
context => StoreEvents.GET(VC_ITEM_STORE_KEY(context.incomingVc)),
context =>
StoreEvents.GET(VCMetadata.fromVC(context.incomingVc).getVcKey()),
{to: context => context.serviceRefs.store},
),
@@ -601,7 +600,10 @@ export const requestMachine =
...existing,
reason: existing.reason.concat(context.incomingVc.reason),
};
return StoreEvents.SET(VC_ITEM_STORE_KEY(updated), updated);
return StoreEvents.SET(
VCMetadata.fromVC(updated).getVcKey(),
updated,
);
},
{to: context => context.serviceRefs.store},
),
@@ -609,7 +611,7 @@ export const requestMachine =
storeVc: send(
context =>
StoreEvents.SET(
VC_ITEM_STORE_KEY(context.incomingVc),
VCMetadata.fromVC(context.incomingVc).getVcKey(),
context.incomingVc,
),
{to: context => context.serviceRefs.store},
@@ -634,7 +636,7 @@ export const requestMachine =
logReceived: send(
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context.incomingVc),
_vcKey: VCMetadata.fromVC(context.incomingVc).getVcKey(),
type: context.receiveLogType,
timestamp: Date.now(),
deviceName:
@@ -646,7 +648,7 @@ export const requestMachine =
sendVcReceived: send(
context => {
return VcEvents.VC_RECEIVED(VC_ITEM_STORE_KEY(context.incomingVc));
return VcEvents.VC_RECEIVED(VCMetadata.fromVC(context.incomingVc));
},
{to: context => context.serviceRefs.vc},
),
@@ -805,9 +807,12 @@ export const requestMachine =
guards: {
hasExistingVc: (context, event) => {
const receivedVcs = event.response as string[];
const vcKey = VC_ITEM_STORE_KEY(context.incomingVc);
return receivedVcs.includes(vcKey);
const receivedVcs = event.vcMetadatas;
const incomingVcMetadata = VCMetadata.fromVC(context.incomingVc);
return receivedVcs?.some(
vcMetadata =>
vcMetadata.getVcKey() == incomingVcMetadata.getVcKey(),
);
},
isMinimumStorageLimitReached: (_context, event) => Boolean(event.data),

View File

@@ -17,7 +17,7 @@ import {getDeviceNameSync} from 'react-native-device-info';
import {VC, VerifiablePresentation} from '../../../types/vc';
import {AppServices} from '../../../shared/GlobalContext';
import {ActivityLogEvents, ActivityLogType} from '../../activityLog';
import {MY_LOGIN_STORE_KEY, VC_ITEM_STORE_KEY} from '../../../shared/constants';
import {MY_LOGIN_STORE_KEY} from '../../../shared/constants';
import {subscribe} from '../../../shared/openIdBLE/walletEventHandler';
import {
check,
@@ -39,6 +39,7 @@ import {WalletDataEvent} from 'react-native-tuvali/lib/typescript/types/events';
import {BLEError} from '../types';
import Storage from '../../../shared/storage';
import {logState} from '../../app';
import {VCMetadata} from '../../../shared/VCMetadata';
import {
getData,
getEndData,
@@ -794,7 +795,7 @@ export const scanMachine =
logShared: send(
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context.selectedVc),
_vcKey: VCMetadata.fromVC(context.selectedVc).getVcKey(),
type: context.selectedVc.shouldVerifyPresence
? 'VC_SHARED_WITH_VERIFICATION_CONSENT'
: context.shareLogType,
@@ -809,7 +810,7 @@ export const scanMachine =
logFailedVerification: send(
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context.selectedVc),
_vcKey: VCMetadata.fromVC(context.selectedVc).getVcKey(),
type: 'PRESENCE_VERIFICATION_FAILED',
timestamp: Date.now(),
deviceName:

View File

@@ -10,6 +10,7 @@ import { createModel } from 'xstate/lib/model';
import { request } from '../shared/request';
import { VcIdType } from '../types/vc';
import { MY_VCS_STORE_KEY } from '../shared/constants';
import { VCMetadata } from '../shared/VCMetadata';
const model = createModel(
{
@@ -20,7 +21,7 @@ const model = createModel(
otpError: '',
transactionId: '',
requestId: '',
VIDs: [] as string[],
VIDsMetadata: [] as VCMetadata[],
},
{
events: {
@@ -29,7 +30,7 @@ const model = createModel(
READY: (idInputRef: TextInput) => ({ idInputRef }),
DISMISS: () => ({}),
SELECT_ID_TYPE: (idType: VcIdType) => ({ idType }),
REVOKE_VCS: (vcKeys: string[]) => ({ vcKeys }),
REVOKE_VCS: (vcMetadatas: VCMetadata[]) => ({ vcMetadatas }),
STORE_RESPONSE: (response: string[]) => ({ response }),
ERROR: (data: Error) => ({ data }),
SUCCESS: () => ({}),
@@ -165,7 +166,7 @@ export const revokeVidsMachine =
}),
setVIDs: model.assign({
VIDs: (_context, event) => event.vcKeys,
VIDsMetadata: (_context, event) => event.vcMetadatas,
}),
setIdBackendError: assign({
@@ -206,12 +207,12 @@ export const revokeVidsMachine =
logRevoked: send(
(context) =>
ActivityLogEvents.LOG_ACTIVITY(
context.VIDs.map((vc) => ({
_vcKey: vc,
context.VIDsMetadata.map((metadata) => ({
_vcKey: metadata.getVcKey(),
type: 'VC_REVOKED',
timestamp: Date.now(),
deviceName: '',
vcLabel: vc.split(':')[2],
vcLabel: metadata.id,
}))
),
{
@@ -221,7 +222,10 @@ export const revokeVidsMachine =
revokeVID: send(
(context) => {
return StoreEvents.REMOVE_ITEMS(MY_VCS_STORE_KEY, context.VIDs);
return StoreEvents.REMOVE_ITEMS(
MY_VCS_STORE_KEY,
context.VIDsMetadata.map((m) => m.getVcKey())
);
},
{
to: (context) => context.serviceRefs.store,
@@ -233,7 +237,7 @@ export const revokeVidsMachine =
requestOtp: async (context) => {
const transactionId = String(new Date().valueOf()).substring(3, 13);
return request('POST', '/residentmobileapp/req/otp', {
individualId: context.VIDs[0].split(':')[2],
individualId: context.VIDsMetadata[0].id,
individualIdType: 'VID',
otpChannel: ['EMAIL', 'PHONE'],
transactionID: transactionId,
@@ -242,20 +246,23 @@ export const revokeVidsMachine =
requestRevoke: (context) => async (callback) => {
await Promise.all(
context.VIDs.map((vid: string) => {
context.VIDsMetadata.map((metadata: VCMetadata) => {
try {
const vidID = vid.split(':')[2];
const transactionId = String(new Date().valueOf()).substring(
3,
13
);
return request('PATCH', `/residentmobileapp/vid/${vidID}`, {
return request(
'PATCH',
`/residentmobileapp/vid/${metadata.id}`,
{
transactionID: transactionId,
vidStatus: 'REVOKED',
individualId: vidID,
individualId: metadata.id,
individualIdType: 'VID',
otp: context.otp,
});
}
);
} catch (error) {
console.log('error.message', error.message);
return error;

View File

@@ -46,7 +46,7 @@ const model = createModel(
}),
UPDATE_ESIGNET_HOST: (esignetHostUrl: string) => ({esignetHostUrl}),
UPDATE_CREDENTIAL_REGISTRY_RESPONSE: (
credentialRegistryResponse: string
credentialRegistryResponse: string,
) => ({
credentialRegistryResponse: credentialRegistryResponse,
}),
@@ -55,7 +55,7 @@ const model = createModel(
CANCEL: () => ({}),
ACCEPT_HARDWARE_SUPPORT_NOT_EXISTS: () => ({}),
},
}
},
);
export const SettingsEvents = model.events;
@@ -165,7 +165,7 @@ export const settingsMachine = model.createMachine(
{
actions: {
requestStoredContext: send(StoreEvents.GET(SETTINGS_STORE_KEY), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
updateDefaults: model.assign({
@@ -179,15 +179,15 @@ export const settingsMachine = model.createMachine(
}),
updatePartialDefaults: model.assign({
appId: (context) => context.appId || generateAppId(),
appId: context => context.appId || generateAppId(),
}),
storeContext: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
return StoreEvents.SET(SETTINGS_STORE_KEY, data);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
setContext: model.assign((context, event) => {
@@ -255,7 +255,7 @@ export const settingsMachine = model.createMachine(
hasPartialData: (_, event) =>
event.response != null && event.response.appId == null,
},
}
},
);
export function createSettingsMachine(serviceRefs: AppServices) {

View File

@@ -12,7 +12,7 @@ import {
import {createModel} from 'xstate/lib/model';
import {generateSecureRandom} from 'react-native-securerandom';
import {log} from 'xstate/lib/actions';
import { MY_VCS_STORE_KEY, VC_ITEM_STORE_KEY_REGEX } from '../shared/constants';
import {MY_VCS_STORE_KEY} from '../shared/constants';
import SecureKeystore from 'react-native-secure-keystore';
import {
AUTH_TIMEOUT,
@@ -24,8 +24,8 @@ import {
HMAC_ALIAS,
isCustomSecureKeystore,
} from '../shared/cryptoutil/cryptoUtil';
import {VCMetadata} from '../shared/VCMetadata';
const vcKeyRegExp = new RegExp(VC_ITEM_STORE_KEY_REGEX);
export const keyinvalidatedString =
'Key Invalidated due to biometric enrollment';
export const tamperedErrorMessageString = 'Data is tampered';
@@ -61,7 +61,7 @@ const model = createModel(
TAMPERED_VC: () => ({}),
RESET_IS_TAMPERED: () => ({}),
},
}
},
);
export const StoreEvents = model.events;
@@ -213,7 +213,7 @@ export const storeMachine =
(_, event) => model.events.STORE_RESPONSE(event.response),
{
to: (_, event) => event.requester,
}
},
),
sendUpdate(),
],
@@ -261,7 +261,7 @@ export const storeMachine =
...event,
requester: meta._event.origin,
}),
{ to: '_store' }
{to: '_store'},
),
setEncryptionKey: model.assign({
@@ -271,13 +271,13 @@ export const storeMachine =
services: {
clear,
hasAndroidEncryptionKey: () => async (callback) => {
hasAndroidEncryptionKey: () => async callback => {
const hasSetCredentials = SecureKeystore.hasAlias(ENCRYPTION_ID);
if (hasSetCredentials) {
try {
await SecureKeystore.encryptData(
DUMMY_KEY_FOR_BIOMETRIC_ALIAS,
'Dummy'
'Dummy',
);
} catch (e) {
if (e.message.includes(keyinvalidatedString)) {
@@ -292,12 +292,12 @@ export const storeMachine =
} else {
callback(
model.events.ERROR(
new Error('Could not get the android Key alias')
)
new Error('Could not get the android Key alias'),
),
);
}
},
checkStorageInitialisedOrNot: () => async (callback) => {
checkStorageInitialisedOrNot: () => async callback => {
const isDirectoryExist = await Storage.isVCStorageInitialised();
if (!isDirectoryExist) {
callback(model.events.READY());
@@ -305,15 +305,15 @@ export const storeMachine =
callback(
model.events.ERROR(
new Error(
'vc directory exists and decryption key is not available'
)
)
'vc directory exists and decryption key is not available',
),
),
);
}
},
store: (context) => (callback, onReceive: Receiver<ForwardedEvent>) => {
onReceive(async (event) => {
store: context => (callback, onReceive: Receiver<ForwardedEvent>) => {
onReceive(async event => {
try {
let response: unknown;
switch (event.type) {
@@ -321,7 +321,7 @@ export const storeMachine =
response = await getItem(
event.key,
null,
context.encryptionKey
context.encryptionKey,
);
break;
}
@@ -334,7 +334,7 @@ export const storeMachine =
await appendItem(
event.key,
event.value,
context.encryptionKey
context.encryptionKey,
);
response = event.value;
break;
@@ -343,7 +343,7 @@ export const storeMachine =
await prependItem(
event.key,
event.value,
context.encryptionKey
context.encryptionKey,
);
response = event.value;
@@ -353,7 +353,7 @@ export const storeMachine =
await updateItem(
event.key,
event.value,
context.encryptionKey
context.encryptionKey,
);
response = event.value;
@@ -363,7 +363,7 @@ export const storeMachine =
await removeItem(
event.key,
event.value,
context.encryptionKey
context.encryptionKey,
);
response = event.value;
break;
@@ -372,7 +372,7 @@ export const storeMachine =
await removeVCMetaData(
event.key,
event.value,
context.encryptionKey
context.encryptionKey,
);
response = event.value;
break;
@@ -381,7 +381,7 @@ export const storeMachine =
await removeItems(
event.key,
event.values,
context.encryptionKey
context.encryptionKey,
);
response = event.values;
break;
@@ -414,7 +414,7 @@ export const storeMachine =
}
});
},
getEncryptionKey: () => async (callback) => {
getEncryptionKey: () => async callback => {
const existingCredentials = await Keychain.getGenericPassword();
if (existingCredentials) {
console.log('Credentials successfully loaded for user');
@@ -423,18 +423,18 @@ export const storeMachine =
console.log('Credentials failed to load for user');
callback(
model.events.ERROR(
new Error('Could not get keychain credentials.')
)
new Error('Could not get keychain credentials.'),
),
);
}
},
generateEncryptionKey: () => async (callback) => {
generateEncryptionKey: () => async callback => {
const randomBytes = await generateSecureRandom(32);
const randomBytesString = binaryToBase64(randomBytes);
if (!isCustomSecureKeystore()) {
const hasSetCredentials = await Keychain.setGenericPassword(
ENCRYPTION_ID,
randomBytesString
randomBytesString,
);
if (hasSetCredentials) {
@@ -442,8 +442,8 @@ export const storeMachine =
} else {
callback(
model.events.ERROR(
new Error('Could not generate keychain credentials.')
)
new Error('Could not generate keychain credentials.'),
),
);
}
} else {
@@ -451,13 +451,13 @@ export const storeMachine =
await SecureKeystore.generateKey(
ENCRYPTION_ID,
isBiometricsEnabled,
AUTH_TIMEOUT
AUTH_TIMEOUT,
);
SecureKeystore.generateHmacshaKey(HMAC_ALIAS);
SecureKeystore.generateKey(
DUMMY_KEY_FOR_BIOMETRIC_ALIAS,
isBiometricsEnabled,
0
0,
);
callback(model.events.KEY_RECEIVED(''));
}
@@ -467,13 +467,13 @@ export const storeMachine =
guards: {
isCustomSecureKeystore: () => isCustomSecureKeystore(),
},
}
},
);
export async function setItem(
key: string,
value: unknown,
encryptionKey: string
encryptionKey: string,
) {
try {
const data = JSON.stringify(value);
@@ -488,17 +488,15 @@ export async function setItem(
export async function getItem(
key: string,
defaultValue: unknown,
encryptionKey: string
encryptionKey: string,
) {
try {
const data = await Storage.getItem(key, encryptionKey);
console.log('getting item for ' + key);
console.log(data);
if (data != null) {
const decryptedData = await decryptJson(encryptionKey, data);
return JSON.parse(decryptedData);
}
if (data === null && vcKeyRegExp.exec(key)) {
if (data === null && VCMetadata.isVCKey(key)) {
await removeItem(key, data, encryptionKey);
throw new Error(tamperedErrorMessageString);
} else {
@@ -520,7 +518,7 @@ export async function getItem(
export async function appendItem(
key: string,
value: unknown,
encryptionKey: string
encryptionKey: string,
) {
try {
const list = await getItem(key, [], encryptionKey);
@@ -534,7 +532,7 @@ export async function appendItem(
export async function prependItem(
key: string,
value: unknown,
encryptionKey: string
encryptionKey: string,
) {
try {
const list = await getItem(key, [], encryptionKey);
@@ -552,20 +550,21 @@ export async function prependItem(
export async function updateItem(
key: string,
value: string,
encryptionKey: string
encryptionKey: string,
) {
// Used for updating VC metadata in the list. Prepends the passed vcmetadata in value and sets ispinned of other vc metadata to false
try {
const list = await getItem(key, [], encryptionKey);
const updatedMetaData = VCMetadata.fromVcMetadataString(value);
const newList = [
value,
...list.map((item) => {
const vc = item.split(':');
if (vc[3] !== value.split(':')[3]) {
vc[4] = 'false';
return vc.join(':');
...list.map(metadataStr => {
const metaData = VCMetadata.fromVcMetadataString(metadataStr);
if (metaData.getVcKey() !== updatedMetaData.getVcKey()) {
return JSON.stringify(metaData);
}
}),
].filter((value) => value != undefined && value !== null);
].filter(value => value != undefined && value !== null);
await setItem(key, newList, encryptionKey);
} catch (e) {
@@ -577,22 +576,18 @@ export async function updateItem(
export async function removeItem(
key: string,
value: string,
encryptionKey: string
encryptionKey: string,
) {
try {
if (value === null && vcKeyRegExp.exec(key)) {
if (value === null && VCMetadata.isVCKey(key)) {
await Storage.removeItem(key);
await removeVCMetaData(MY_VCS_STORE_KEY, key, encryptionKey);
} else {
} else if (key === MY_VCS_STORE_KEY) {
const data = await Storage.getItem(key, encryptionKey);
const decryptedData = await decryptJson(encryptionKey, data);
const list = JSON.parse(decryptedData);
const vcKeyArray = value.split(':');
const finalVcKeyArray = vcKeyArray.pop();
const finalVcKey = vcKeyArray.join(':');
//console.log('finalVcKeyArray', finalVcKeyArray);
const newList = list.filter((vc: string) => {
return !vc.includes(finalVcKey);
const list = JSON.parse(decryptedData) as string[];
const newList = list.filter((str: string) => {
return VCMetadata.fromVcMetadataString(str).getVcKey() !== value;
});
await setItem(key, newList, encryptionKey);
@@ -606,15 +601,15 @@ export async function removeItem(
export async function removeVCMetaData(
key: string,
value: string,
encryptionKey: string
vcKey: string,
encryptionKey: string,
) {
try {
const data = await Storage.getItem(key, encryptionKey);
const decryptedData = await decryptJson(encryptionKey, data);
const list = JSON.parse(decryptedData);
const newList = list.filter((vc: string) => {
return !vc.includes(value);
const list = JSON.parse(decryptedData) as string[];
const newList = list.filter((str: string) => {
return VCMetadata.fromVcMetadataString(str).getVcKey() !== vcKey;
});
await setItem(key, newList, encryptionKey);
@@ -627,20 +622,16 @@ export async function removeVCMetaData(
export async function removeItems(
key: string,
values: string[],
encryptionKey: string
encryptionKey: string,
) {
try {
const data = await Storage.getItem(key, encryptionKey);
const decryptedData = await decryptJson(encryptionKey, data);
const list = JSON.parse(decryptedData);
const newList = list.filter(function (vc: string) {
return !values.find(function (vcKey: string) {
const vcKeyArray = vcKey.split(':');
const finalVcKeyArray = vcKeyArray.pop();
const finalVcKey = vcKeyArray.join(':');
return vc.includes(finalVcKey);
});
});
const list = JSON.parse(decryptedData) as string[];
const newList = list.filter(
(str: string) =>
!values.includes(VCMetadata.fromVcMetadataString(str).getVcKey()),
);
await setItem(key, newList, encryptionKey);
} catch (e) {

View File

@@ -1,35 +1,30 @@
import { EventFrom, StateFrom } from 'xstate';
import { send, sendParent } from 'xstate';
import {EventFrom, send, sendParent, StateFrom} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {StoreEvents} from './store';
import {VC} from '../types/vc';
import {AppServices} from '../shared/GlobalContext';
import {log, respond} from 'xstate/lib/actions';
import {VcItemEvents} from './vcItem';
import {
MY_VCS_STORE_KEY,
RECEIVED_VCS_STORE_KEY,
VC_ITEM_STORE_KEY,
isSameVC,
} from '../shared/constants';
import {MY_VCS_STORE_KEY, RECEIVED_VCS_STORE_KEY} from '../shared/constants';
import {parseMetadatas, VCMetadata} from '../shared/VCMetadata';
const model = createModel(
{
serviceRefs: {} as AppServices,
myVcs: [] as string[],
receivedVcs: [] as string[],
myVcs: [] as VCMetadata[],
receivedVcs: [] as VCMetadata[],
vcs: {} as Record<string, VC>,
},
{
events: {
VIEW_VC: (vc: VC) => ({vc}),
GET_VC_ITEM: (vcKey: string) => ({ vcKey }),
GET_VC_ITEM: (vcMetadata: VCMetadata) => ({vcMetadata}),
STORE_RESPONSE: (response: unknown) => ({response}),
STORE_ERROR: (error: Error) => ({error}),
VC_ADDED: (vcKey: string) => ({ vcKey }),
REMOVE_VC_FROM_CONTEXT: (vcKey: string) => ({ vcKey }),
VC_UPDATED: (vcKey: string) => ({ vcKey }),
VC_RECEIVED: (vcKey: string) => ({ vcKey }),
VC_ADDED: (vcMetadata: VCMetadata) => ({vcMetadata}),
REMOVE_VC_FROM_CONTEXT: (vcMetadata: VCMetadata) => ({vcMetadata}),
VC_METADATA_UPDATED: (vcMetadata: VCMetadata) => ({vcMetadata}),
VC_RECEIVED: (vcMetadata: VCMetadata) => ({vcMetadata}),
VC_DOWNLOADED: (vc: VC) => ({vc}),
VC_UPDATE: (vc: VC) => ({vc}),
REFRESH_MY_VCS: () => ({}),
@@ -37,7 +32,7 @@ const model = createModel(
REFRESH_RECEIVED_VCS: () => ({}),
GET_RECEIVED_VCS: () => ({}),
},
}
},
);
export const VcEvents = model.events;
@@ -145,8 +140,8 @@ export const vcMachine =
REMOVE_VC_FROM_CONTEXT: {
actions: 'removeVcFromMyVcs',
},
VC_UPDATED: {
actions: ['updateMyVcs', 'setUpdateVc'],
VC_METADATA_UPDATED: {
actions: ['updateMyVcs', 'setUpdatedVcMetadatas'],
},
VC_DOWNLOADED: {
actions: 'setDownloadedVc',
@@ -169,79 +164,80 @@ export const vcMachine =
},
{
actions: {
getReceivedVcsResponse: respond((context) => ({
getReceivedVcsResponse: respond(context => ({
type: 'VC_RESPONSE',
response: context.receivedVcs,
response: context.receivedVcs || [],
})),
getVcItemResponse: respond((context, event) => {
const vc = context.vcs[event.vcKey];
const vc = context.vcs[event.vcMetadata?.getVcKey()];
return VcItemEvents.GET_VC_RESPONSE(vc);
}),
loadMyVcs: send(StoreEvents.GET(MY_VCS_STORE_KEY), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
loadReceivedVcs: send(StoreEvents.GET(RECEIVED_VCS_STORE_KEY), {
to: (context) => context.serviceRefs.store,
to: context => context.serviceRefs.store,
}),
setMyVcs: model.assign({
myVcs: (_context, event) => (event.response || []) as string[],
myVcs: (_context, event) => {
return parseMetadatas((event.response || []) as object[]);
},
}),
setReceivedVcs: model.assign({
receivedVcs: (_context, event) => (event.response || []) as string[],
receivedVcs: (_context, event) => {
return parseMetadatas((event.response || []) as object[]);
},
}),
setDownloadedVc: (context, event) => {
context.vcs[VC_ITEM_STORE_KEY(event.vc)] = event.vc;
const vcUniqueId = VCMetadata.fromVC(event.vc).getVcKey();
context.vcs[vcUniqueId] = event.vc;
},
setVcUpdate: (context, event) => {
Object.keys(context.vcs).map((vcKey) => {
if (isSameVC(vcKey, VC_ITEM_STORE_KEY(event.vc))) {
context.vcs[VC_ITEM_STORE_KEY(event.vc)] = context.vcs[vcKey];
delete context.vcs[vcKey];
return context.vcs[VC_ITEM_STORE_KEY(event.vc)];
Object.keys(context.vcs).map(vcUniqueId => {
const eventVCMetadata = VCMetadata.fromVC(event.vc);
if (vcUniqueId === eventVCMetadata.getVcKey()) {
context.vcs[eventVCMetadata.getVcKey()] = context.vcs[vcUniqueId];
delete context.vcs[vcUniqueId];
return context.vcs[eventVCMetadata.getVcKey()];
}
});
},
setUpdateVc: send(
(_context, event) => {
return StoreEvents.UPDATE(MY_VCS_STORE_KEY, event.vcKey);
setUpdatedVcMetadatas: send(
_context => {
return StoreEvents.SET(MY_VCS_STORE_KEY, _context.myVcs);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
prependToMyVcs: model.assign({
myVcs: (context, event) => [event.vcKey, ...context.myVcs],
myVcs: (context, event) => [event.vcMetadata, ...context.myVcs],
}),
removeVcFromMyVcs: model.assign({
myVcs: (context, event) =>
context.myVcs.filter((vc: string) => !vc.includes(event.vcKey)),
context.myVcs.filter(
(vc: VCMetadata) => !vc.equals(event.vcMetadata),
),
}),
updateMyVcs: model.assign({
myVcs: (context, event) =>
[
event.vcKey,
...context.myVcs.map((value) => {
const vc = value.split(':');
if (vc[3] !== event.vcKey.split(':')[3]) {
vc[4] = 'false';
return vc.join(':');
}
}),
].filter((value) => value != undefined),
myVcs: (context, event) => [
...getUpdatedVCMetadatas(context.myVcs, event.vcMetadata),
],
}),
prependToReceivedVcs: model.assign({
receivedVcs: (context, event) => [
event.vcKey,
event.vcMetadata,
...context.receivedVcs,
],
}),
@@ -249,8 +245,10 @@ export const vcMachine =
moveExistingVcToTop: model.assign({
receivedVcs: (context, event) => {
return [
event.vcKey,
...context.receivedVcs.filter((value) => value !== event.vcKey),
event.vcMetadata,
...context.receivedVcs.filter(value =>
value.equals(event.vcMetadata),
),
];
},
}),
@@ -258,9 +256,11 @@ export const vcMachine =
guards: {
hasExistingReceivedVc: (context, event) =>
context.receivedVcs.includes(event.vcKey),
context.receivedVcs.find(vcMetadata =>
vcMetadata.equals(event.vcMetadata),
) != null,
},
},
}
);
export function createVcMachine(serviceRefs: AppServices) {
@@ -272,17 +272,17 @@ export function createVcMachine(serviceRefs: AppServices) {
type State = StateFrom<typeof vcMachine>;
export function selectMyVcs(state: State) {
export function selectMyVcsMetadata(state: State): VCMetadata[] {
return state.context.myVcs;
}
export function selectShareableVcs(state: State) {
export function selectShareableVcsMetadata(state: State): VCMetadata[] {
return state.context.myVcs.filter(
(vcKey) => state.context.vcs[vcKey]?.credential != null
vcMetadata => state.context.vcs[vcMetadata.getVcKey()]?.credential != null,
);
}
export function selectReceivedVcs(state: State) {
export function selectReceivedVcsMetadata(state: State): VCMetadata[] {
return state.context.receivedVcs;
}
@@ -297,15 +297,34 @@ export function selectIsRefreshingReceivedVcs(state: State) {
/*
this methods returns all the binded vc's in the wallet.
*/
export function selectBindedVcs(state: State) {
return (state.context.myVcs as Array<string>).filter((key) => {
const walletBindingResponse = state.context.vcs[key]?.walletBindingResponse;
export function selectBindedVcsMetadata(state: State): VCMetadata[] {
return state.context.myVcs.filter(vcMetadata => {
const walletBindingResponse =
state.context.vcs[vcMetadata.getVcKey()]?.walletBindingResponse;
return (
!isEmpty(walletBindingResponse) &&
!isEmpty(walletBindingResponse?.walletBindingId)
);
});
}
function getUpdatedVCMetadatas(
existingVCMetadatas: VCMetadata[],
updatedVcMetadata: VCMetadata,
) {
const isPinStatusUpdated = updatedVcMetadata.isPinned;
return existingVCMetadatas.map(value => {
if (value.equals(updatedVcMetadata)) {
return updatedVcMetadata;
} else if (isPinStatusUpdated) {
return new VCMetadata({...value, isPinned: false});
} else {
return value;
}
});
}
function isEmpty(object) {
return object == null || object == '' || object == undefined;
}

View File

@@ -1,11 +1,6 @@
import {assign, ErrorPlatformEvent, EventFrom, send, StateFrom} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {
MIMOTO_BASE_URL,
MY_VCS_STORE_KEY,
VC_ITEM_STORE_KEY,
VC_ITEM_STORE_KEY_AFTER_DOWNLOAD,
} from '../shared/constants';
import {HOST, MIMOTO_BASE_URL, MY_VCS_STORE_KEY} from '../shared/constants';
import {AppServices} from '../shared/GlobalContext';
import {CredentialDownloadResponse, request} from '../shared/request';
import {
@@ -34,6 +29,7 @@ import getAllConfigurations, {
import {VcEvents} from './vc';
import i18n from '../i18n';
import SecureKeystore from 'react-native-secure-keystore';
import {VCMetadata} from '../shared/VCMetadata';
import {
sendStartEvent,
getData,
@@ -47,7 +43,7 @@ const model = createModel(
id: '',
idType: '' as VcIdType,
tag: '',
vcKey: '' as string,
vcMetadata: {} as VCMetadata,
myVcs: [] as string[],
generatedOn: null as Date,
credential: null as DecodedCredential,
@@ -71,7 +67,6 @@ const model = createModel(
walletBindingError: '',
publicKey: '',
privateKey: '',
hashedId: '',
},
{
events: {
@@ -98,9 +93,9 @@ const model = createModel(
PIN_CARD: () => ({}),
KEBAB_POPUP: () => ({}),
SHOW_ACTIVITY: () => ({}),
REMOVE: (vcKey: string) => ({ vcKey }),
REMOVE: (vcMetadata: VCMetadata) => ({vcMetadata}),
},
},
}
);
export const VcItemEvents = model.events;
@@ -207,11 +202,7 @@ export const vcItemMachine =
},
],
CREDENTIAL_DOWNLOADED: {
actions: [
'setStoreVerifiableCredential',
'storeContext',
'editVcKey',
],
actions: ['setStoreVerifiableCredential', 'storeContext'],
},
STORE_RESPONSE: {
actions: [
@@ -273,14 +264,11 @@ export const vcItemMachine =
},
},
pinCard: {
entry: 'storeContext',
on: {
STORE_RESPONSE: {
actions: ['sendVcUpdated', 'VcUpdated'],
entry: 'sendVcUpdated',
always: {
target: 'idle',
},
},
},
kebabPopUp: {
on: {
DISMISS: {
@@ -763,7 +751,7 @@ export const vcItemMachine =
},
{
actions: {
setVerifiableCredential: assign((context) => {
setVerifiableCredential: assign(context => {
return {
...context,
verifiableCredential: {
@@ -785,33 +773,31 @@ export const vcItemMachine =
}),
removeVcMetaDataFromStorage: send(
(context) => {
const { serviceRefs, ...data } = context;
context => {
return StoreEvents.REMOVE_VC_METADATA(
MY_VCS_STORE_KEY,
VC_ITEM_STORE_KEY(context)
new VCMetadata(context).getVcKey(),
);
},
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
removeVcMetaDataFromVcMachine: send(
(context, _event) => {
const { serviceRefs, ...data } = context;
context => {
return {
type: 'REMOVE_VC_FROM_CONTEXT',
vcKey: VC_ITEM_STORE_KEY(context),
vcMetadata: new VCMetadata(context),
};
},
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
setWalletBindingError: assign({
walletBindingError: (context, event) =>
walletBindingError: () =>
i18n.t(`errors.genericError`, {
ns: 'common',
}),
@@ -843,7 +829,7 @@ export const vcItemMachine =
event.data as WalletBindingResponse,
}),
setPinCard: assign((context) => {
setPinCard: assign(context => {
return {
...context,
isPinned: !context.isPinned,
@@ -851,47 +837,46 @@ export const vcItemMachine =
}),
sendVcUpdated: send(
(_context, event) =>
VcEvents.VC_UPDATED(VC_ITEM_STORE_KEY(event.response) as string),
context => VcEvents.VC_METADATA_UPDATED(new VCMetadata(context)),
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
updateVc: send(
(context) => {
context => {
const {serviceRefs, ...vc} = context;
return {type: 'VC_DOWNLOADED', vc};
},
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
VcUpdated: send(
(context) => {
context => {
const {serviceRefs, ...vc} = context;
return {type: 'VC_UPDATE', vc};
},
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
setThumbprintForWalletBindingId: send(
(context) => {
context => {
const {walletBindingResponse} = context;
const walletBindingIdKey = getBindingCertificateConstant(
walletBindingResponse.walletBindingId
walletBindingResponse.walletBindingId,
);
return StoreEvents.SET(
walletBindingIdKey,
walletBindingResponse.thumbprint
walletBindingResponse.thumbprint,
);
},
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
removedVc: send(
@@ -899,46 +884,38 @@ export const vcItemMachine =
type: 'REFRESH_MY_VCS',
}),
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
requestVcContext: send(
(context) => ({
context => ({
type: 'GET_VC_ITEM',
vcKey: VC_ITEM_STORE_KEY(context),
vcMetadata: new VCMetadata(context),
}),
{
to: (context) => context.serviceRefs.vc,
}
to: context => context.serviceRefs.vc,
},
),
requestStoredContext: send(
(context) => StoreEvents.GET(VC_ITEM_STORE_KEY(context)),
context => StoreEvents.GET(new VCMetadata(context).getVcKey()),
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
storeContext: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
data.credentialRegistry = MIMOTO_BASE_URL;
return StoreEvents.SET(VC_ITEM_STORE_KEY(context), data);
return StoreEvents.SET(new VCMetadata(context).getVcKey(), data);
},
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
editVcKey: send((context) => {
const { serviceRefs, ...data } = context;
return StoreEvents.SET(
VC_ITEM_STORE_KEY_AFTER_DOWNLOAD(context),
data
);
}),
setTag: model.assign({
tag: (_, event) => event.tag,
}),
@@ -958,11 +935,11 @@ export const vcItemMachine =
}),
storeTag: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
return StoreEvents.SET(VC_ITEM_STORE_KEY(context), data);
return StoreEvents.SET(new VCMetadata(context).getVcKey(), data);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
setCredential: model.assign((context, event) => {
@@ -976,10 +953,10 @@ export const vcItemMachine =
}),
logDownloaded: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
return ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(data),
_vcKey: VCMetadata.fromVC(data).getVcKey(),
type: 'VC_DOWNLOADED',
timestamp: Date.now(),
deviceName: '',
@@ -987,65 +964,65 @@ export const vcItemMachine =
});
},
{
to: (context) => context.serviceRefs.activityLog,
}
to: context => context.serviceRefs.activityLog,
},
),
logWalletBindingSuccess: send(
(context, event) =>
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context),
_vcKey: new VCMetadata(context).getVcKey(),
type: 'WALLET_BINDING_SUCCESSFULL',
timestamp: Date.now(),
deviceName: '',
vcLabel: context.tag || context.id,
}),
{
to: (context) => context.serviceRefs.activityLog,
}
to: context => context.serviceRefs.activityLog,
},
),
logWalletBindingFailure: send(
(context, event) =>
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context),
_vcKey: new VCMetadata(context).getVcKey(),
type: 'WALLET_BINDING_FAILURE',
timestamp: Date.now(),
deviceName: '',
vcLabel: context.tag || context.id,
}),
{
to: (context) => context.serviceRefs.activityLog,
}
to: context => context.serviceRefs.activityLog,
},
),
logRevoked: send(
(context) =>
context =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context),
_vcKey: new VCMetadata(context).getVcKey(),
type: 'VC_REVOKED',
timestamp: Date.now(),
deviceName: '',
vcLabel: context.tag || context.id,
}),
{
to: (context) => context.serviceRefs.activityLog,
}
to: context => context.serviceRefs.activityLog,
},
),
revokeVID: send(
(context) => {
context => {
return StoreEvents.REMOVE(
MY_VCS_STORE_KEY,
VC_ITEM_STORE_KEY(context)
new VCMetadata(context).getVcKey(),
);
},
{
to: (context) => context.serviceRefs.store,
}
to: context => context.serviceRefs.store,
},
),
markVcValid: assign((context) => {
markVcValid: assign(context => {
return {
...context,
isVerified: true,
@@ -1064,7 +1041,7 @@ export const vcItemMachine =
}),
setVcKey: model.assign({
vcKey: (_, event) => event.vcKey,
vcMetadata: (_, event) => event.vcMetadata,
}),
setOtpError: assign({
@@ -1075,7 +1052,7 @@ export const vcItemMachine =
clearOtp: assign({otp: ''}),
setLock: assign({
locked: (context) => !context.locked,
locked: context => !context.locked,
}),
setRevoke: assign({
@@ -1083,44 +1060,47 @@ export const vcItemMachine =
}),
storeLock: send(
(context) => {
context => {
const {serviceRefs, ...data} = context;
return StoreEvents.SET(VC_ITEM_STORE_KEY(context), data);
return StoreEvents.SET(new VCMetadata(context).getVcKey(), data);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
removeVcItem: send(
(_context, event) => {
return StoreEvents.REMOVE(MY_VCS_STORE_KEY, _context.vcKey);
_context => {
return StoreEvents.REMOVE(
MY_VCS_STORE_KEY,
_context.vcMetadata.getVcKey(),
);
},
{ to: (context) => context.serviceRefs.store }
{to: context => context.serviceRefs.store},
),
logVCremoved: send(
(context, _) =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context),
_vcKey: new VCMetadata(context).getVcKey(),
type: 'VC_REMOVED',
timestamp: Date.now(),
deviceName: '',
vcLabel: context.id,
}),
{
to: (context) => context.serviceRefs.activityLog,
}
to: context => context.serviceRefs.activityLog,
},
),
},
services: {
checkDownloadExpiryLimit: async (context) => {
checkDownloadExpiryLimit: async context => {
var resp = await getAllConfigurations();
const maxLimit: number = resp.vcDownloadMaxRetry;
const vcDownloadPoolInterval: number = resp.vcDownloadPoolInterval;
console.log(maxLimit);
if (maxLimit <= context.downloadCounter) {
throw new Error(
'Download limit expired for request id: ' + context.requestId
'Download limit expired for request id: ' + context.requestId,
);
}
@@ -1132,7 +1112,7 @@ export const vcItemMachine =
return downloadProps;
},
addWalletBindnigId: async (context) => {
addWalletBindnigId: async context => {
const response = await request(
'POST',
'/residentmobileapp/wallet-binding',
@@ -1152,12 +1132,12 @@ export const vcItemMachine =
},
],
},
}
},
);
const certificate = response.response.certificate;
await savePrivateKey(
getBindingCertificateConstant(context.id),
certificate
certificate,
);
const walletResponse: WalletBindingResponse = {
@@ -1169,10 +1149,10 @@ export const vcItemMachine =
return walletResponse;
},
updatePrivateKey: async (context) => {
updatePrivateKey: async context => {
const hasSetPrivateKey: boolean = await savePrivateKey(
context.walletBindingResponse.walletBindingId,
context.privateKey
context.privateKey,
);
if (!hasSetPrivateKey) {
throw new Error('Could not store private key in keystore.');
@@ -1180,7 +1160,7 @@ export const vcItemMachine =
return '';
},
generateKeyPair: async (context) => {
generateKeyPair: async context => {
if (!isCustomSecureKeystore()) {
return await generateKeys();
}
@@ -1188,11 +1168,11 @@ export const vcItemMachine =
return SecureKeystore.generateKeyPair(
context.id,
isBiometricsEnabled,
0
0,
);
},
requestBindingOtp: async (context) => {
requestBindingOtp: async context => {
const response = await request(
'POST',
'/residentmobileapp/binding-otp',
@@ -1202,24 +1182,24 @@ export const vcItemMachine =
individualId: context.id,
otpChannels: ['EMAIL', 'PHONE'],
},
}
},
);
if (response.response == null) {
throw new Error('Could not process request');
}
},
checkStatus: (context) => (callback, onReceive) => {
checkStatus: context => (callback, onReceive) => {
const pollInterval = setInterval(
() => callback(model.events.POLL()),
context.downloadInterval
context.downloadInterval,
);
onReceive(async (event) => {
onReceive(async event => {
if (event.type === 'POLL_STATUS') {
const response = await request(
'GET',
`/residentmobileapp/credentialshare/request/status/${context.requestId}`
`/residentmobileapp/credentialshare/request/status/${context.requestId}`,
);
switch (response.response?.statusCode) {
case 'NEW':
@@ -1235,13 +1215,13 @@ export const vcItemMachine =
return () => clearInterval(pollInterval);
},
downloadCredential: (context) => (callback, onReceive) => {
downloadCredential: context => (callback, onReceive) => {
const pollInterval = setInterval(
() => callback(model.events.POLL()),
context.downloadInterval
context.downloadInterval,
);
onReceive(async (event) => {
onReceive(async event => {
if (event.type === 'POLL_DOWNLOAD') {
const response: CredentialDownloadResponse = await request(
'POST',
@@ -1249,7 +1229,7 @@ export const vcItemMachine =
{
individualId: context.id,
requestId: context.requestId,
}
},
);
callback(
@@ -1267,7 +1247,7 @@ export const vcItemMachine =
locked: context.locked,
walletBindingResponse: null,
credentialRegistry: '',
})
}),
);
}
});
@@ -1275,11 +1255,11 @@ export const vcItemMachine =
return () => clearInterval(pollInterval);
},
verifyCredential: async (context) => {
verifyCredential: async context => {
return verifyCredential(context.verifiableCredential);
},
requestOtp: async (context) => {
requestOtp: async context => {
try {
return request('POST', '/residentmobileapp/req/otp', {
individualId: context.id,
@@ -1292,7 +1272,7 @@ export const vcItemMachine =
}
},
requestLock: async (context) => {
requestLock: async context => {
let response = null;
if (context.locked) {
response = await request(
@@ -1305,7 +1285,7 @@ export const vcItemMachine =
transactionID: context.transactionId,
authType: ['bio'],
unlockForSeconds: '120',
}
},
);
} else {
response = await request(
@@ -1317,13 +1297,13 @@ export const vcItemMachine =
otp: context.otp,
transactionID: context.transactionId,
authType: ['bio'],
}
},
);
}
return response.response;
},
requestRevoke: async (context) => {
requestRevoke: async context => {
try {
return request('PATCH', `/residentmobileapp/vid/${context.id}`, {
transactionID: context.transactionId,
@@ -1346,32 +1326,30 @@ export const vcItemMachine =
return vc?.credential != null && vc?.verifiableCredential != null;
},
isDownloadAllowed: (_context, event) => {
isDownloadAllowed: _context => {
return _context.downloadCounter <= _context.maxDownloadCount;
},
isVcValid: (context) => {
isVcValid: context => {
return context.isVerified;
},
isCustomSecureKeystore: () => isCustomSecureKeystore(),
},
}
},
);
export const createVcItemMachine = (
serviceRefs: AppServices,
vcKey: string
vcMetadata: VCMetadata,
) => {
const [, idType, hashedId, requestId, isPinned, id] = vcKey.split(':');
return vcItemMachine.withContext({
...vcItemMachine.context,
serviceRefs,
id,
idType: idType as VcIdType,
requestId,
isPinned: isPinned == 'true' ? true : false,
hashedId,
id: vcMetadata.id,
idType: vcMetadata.idType as VcIdType,
requestId: vcMetadata.requestId,
isPinned: vcMetadata.isPinned,
});
};

View File

@@ -12,7 +12,7 @@ module.exports = (async () => {
assetPlugins: ['expo-asset/tools/hashAssetFiles'],
},
resolver: {
assetExts: assetExts.filter((ext) => ext !== 'svg'),
assetExts: assetExts.filter(ext => ext !== 'svg'),
sourceExts: [...sourceExts, 'svg'],
},
};

View File

@@ -9,14 +9,9 @@ import {
} from 'xstate';
import {createModel} from 'xstate/lib/model';
import {BackendResponseError, request} from '../../../shared/request';
import {
argon2iConfigForUinVid,
argon2iSalt,
VC_ITEM_STORE_KEY,
} from '../../../shared/constants';
import {VcIdType} from '../../../types/vc';
import i18n from '../../../i18n';
import { hashData } from '../../../shared/commonUtil';
import {VCMetadata} from '../../../shared/VCMetadata';
const model = createModel(
{
@@ -29,7 +24,6 @@ const model = createModel(
transactionId: '',
requestId: '',
isPinned: false,
hashedId: '',
},
{
events: {
@@ -41,7 +35,7 @@ const model = createModel(
DISMISS: () => ({}),
SELECT_ID_TYPE: (idType: VcIdType) => ({idType}),
},
}
},
);
export const AddVcModalEvents = model.events;
@@ -212,7 +206,7 @@ export const AddVcModalMachine =
onDone: [
{
actions: 'setRequestId',
target: 'calculatingHashedId',
target: 'done',
},
],
onError: [
@@ -228,18 +222,9 @@ export const AddVcModalMachine =
],
},
},
calculatingHashedId: {
invoke: {
src: 'calculateHashedId',
onDone: {
actions: 'setHashedId',
target: 'done',
},
},
},
done: {
type: 'final',
data: (context) => VC_ITEM_STORE_KEY(context),
data: context => new VCMetadata(context),
},
},
},
@@ -296,11 +281,6 @@ export const AddVcModalMachine =
clearId: model.assign({id: ''}),
setHashedId: model.assign({
hashedId: (_context, event) =>
(event as DoneInvokeEvent<string>).data,
}),
clearIdError: model.assign({idError: ''}),
setIdErrorEmpty: model.assign({
@@ -337,11 +317,11 @@ export const AddVcModalMachine =
clearOtp: assign({otp: ''}),
focusInput: (context) => context.idInputRef.focus(),
focusInput: context => context.idInputRef.focus(),
},
services: {
requestOtp: async (context) => {
requestOtp: async context => {
return request('POST', '/residentmobileapp/req/otp', {
id: 'mosip.identity.otp.internal',
individualId: context.id,
@@ -353,9 +333,9 @@ export const AddVcModalMachine =
});
},
requestCredential: async (context) => {
requestCredential: async context => {
// force wait to fix issue with hanging overlay
await new Promise((resolve) => setTimeout(resolve, 1000));
await new Promise(resolve => setTimeout(resolve, 1000));
const response = await request(
'POST',
@@ -365,21 +345,10 @@ export const AddVcModalMachine =
individualIdType: context.idType,
otp: context.otp,
transactionID: context.transactionId,
}
},
);
return response.response.requestId;
},
calculateHashedId: async (context) => {
const value = context.id;
const hashedid = await hashData(
value,
argon2iSalt,
argon2iConfigForUinVid
);
context.hashedId = hashedid;
return hashedid;
},
},
guards: {
@@ -393,10 +362,10 @@ export const AddVcModalMachine =
isIdInvalid: (_context, event: unknown) =>
['IDA-MLC-009', 'RES-SER-29', 'IDA-MLC-018'].includes(
(event as BackendResponseError).name
(event as BackendResponseError).name,
),
},
}
},
);
type State = StateFrom<typeof AddVcModalMachine>;

View File

@@ -2,7 +2,7 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
internalEvents: {
'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]': {
type: 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
data: unknown;
@@ -30,19 +30,19 @@ export interface Typegen0 {
};
'xstate.init': {type: 'xstate.init'};
};
'invokeSrcNameMap': {
invokeSrcNameMap: {
requestCredential: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
requestOtp:
| 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]'
| 'done.invoke.AddVcModal.acceptingOtpInput.resendOTP:invocation[0]';
};
'missingImplementations': {
missingImplementations: {
actions: never;
delays: never;
guards: never;
services: never;
};
'eventsCausingActions': {
eventsCausingActions: {
clearId: 'SELECT_ID_TYPE';
clearIdError: 'INPUT_ID' | 'SELECT_ID_TYPE' | 'VALIDATE_INPUT';
clearOtp:
@@ -79,17 +79,17 @@ export interface Typegen0 {
| 'error.platform.AddVcModal.requestingCredential:invocation[0]'
| 'xstate.init';
};
'eventsCausingDelays': {};
'eventsCausingGuards': {
eventsCausingDelays: {};
eventsCausingGuards: {
isEmptyId: 'VALIDATE_INPUT';
isIdInvalid: 'error.platform.AddVcModal.requestingCredential:invocation[0]';
isWrongIdFormat: 'VALIDATE_INPUT';
};
'eventsCausingServices': {
eventsCausingServices: {
requestCredential: 'INPUT_OTP';
requestOtp: 'RESEND_OTP' | 'VALIDATE_INPUT';
};
'matchesStates':
matchesStates:
| 'acceptingIdInput'
| 'acceptingIdInput.focusing'
| 'acceptingIdInput.idle'
@@ -114,5 +114,5 @@ export interface Typegen0 {
| {invalid?: 'backend' | 'empty' | 'format'};
acceptingOtpInput?: 'idle' | 'resendOTP';
};
'tags': never;
tags: never;
}

View File

@@ -8,7 +8,7 @@ import {ActorRefFrom} from 'xstate';
import {vcItemMachine} from '../../../machines/vcItem';
import {useKebabPopUp} from '../../../components/KebabPopUpController';
import {Theme} from '../../../components/ui/styleUtils';
import {isSameVC} from '../../../shared/constants';
import {VCMetadata} from '../../../shared/VCMetadata';
import testIDProps from '../../../shared/commonUtil';
export const HistoryTab: React.FC<HistoryTabProps> = props => {
@@ -28,21 +28,18 @@ export const HistoryTab: React.FC<HistoryTabProps> = props => {
</ListItem.Title>
</ListItem.Content>
<Modal
headerLabel={props.vcKey.split(':')[5]}
headerLabel={props.vcMetadata.id}
isVisible={controller.isShowActivities}
onDismiss={controller.DISMISS}>
<Column fill>
{controller.activities.map(activity => {
const vcKeyMatch = isSameVC(activity._vcKey, props.vcKey);
if (vcKeyMatch) {
return (
{controller.activities
.filter(activity => activity._vcKey === props.vcMetadata.getVcKey())
.map(activity => (
<ActivityLogText
key={`${activity.timestamp}-${activity._vcKey}`}
activity={activity}
/>
);
}
})}
))}
{controller.activities.length === 0 && (
<Centered fill>
<Icon
@@ -69,6 +66,6 @@ export const HistoryTab: React.FC<HistoryTabProps> = props => {
export interface HistoryTabProps {
testID?: string;
label: string;
vcKey: string;
vcMetadata: VCMetadata;
service: ActorRefFrom<typeof vcItemMachine>;
}

View File

@@ -18,7 +18,7 @@ import { GET_INDIVIDUAL_ID } from '../../../shared/constants';
import {MessageOverlay} from '../../../components/MessageOverlay';
import testIDProps from '../../../shared/commonUtil';
export const IdInputModal: React.FC<IdInputModalProps> = (props) => {
export const IdInputModal: React.FC<IdInputModalProps> = props => {
const {t} = useTranslation('IdInputModal');
const controller = useIdInputModal(props);

View File

@@ -1,7 +1,7 @@
import { useSelector, useInterpret } from '@xstate/react';
import { useContext, useRef, useState } from 'react';
import { GlobalContext } from '../../../shared/GlobalContext';
import { selectMyVcs, VcEvents } from '../../../machines/vc';
import { selectMyVcsMetadata, VcEvents } from '../../../machines/vc';
import {
createVcItemMachine,
isShowingBindingWarning,
@@ -24,7 +24,7 @@ export function useWalletBinding(props) {
const machine = useRef(
createVcItemMachine(
appService.getSnapshot().context.serviceRefs,
props.vcKey
props.vcMetadata
)
);
@@ -32,7 +32,7 @@ export function useWalletBinding(props) {
const vcService = appService.children.get('vc');
const vcKeys = useSelector(vcService, selectMyVcs);
const vcsMetadata = useSelector(vcService, selectMyVcsMetadata);
const otpError = useSelector(bindingService, selectOtpError);
const [isRevoking, setRevoking] = useState(false);

View File

@@ -1,7 +1,7 @@
import React from 'react';
import {Button, Column, Row, Text} from '../../components/ui';
import {Theme} from '../../components/ui/styleUtils';
import {RefreshControl, Image, View} from 'react-native';
import {Image, RefreshControl, View} from 'react-native';
import {useMyVcsTab} from './MyVcsTabController';
import {HomeScreenTabProps} from './HomeScreen';
import {AddVcModal} from './MyVcs/AddVcModal';
@@ -14,11 +14,19 @@ import {
MessageOverlay,
} from '../../components/MessageOverlay';
import {Icon} from 'react-native-elements';
import {groupBy} from '../../shared/javascript';
const pinIconProps = {iconName: 'pushpin', iconType: 'antdesign'};
export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
const {t} = useTranslation('MyVcsTab');
const controller = useMyVcsTab(props);
const storeErrorTranslationPath = 'errors.savingFailed';
const [pinned, unpinned] = groupBy(
controller.vcMetadatas,
vcMetadata => vcMetadata.isPinned,
);
const vcMetadataOrderedByPinStatus = pinned.concat(unpinned);
const getId = () => {
controller.DISMISS();
@@ -64,7 +72,7 @@ export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
<Column fill style={{display: props.isVisible ? 'flex' : 'none'}}>
{controller.isRequestSuccessful && <DownloadingVcPopUp />}
<Column fill pY={18} pX={15}>
{controller.vcKeys.length > 0 && (
{vcMetadataOrderedByPinStatus.length > 0 && (
<React.Fragment>
<Column
scroll
@@ -76,31 +84,17 @@ export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
onRefresh={controller.REFRESH}
/>
}>
{controller.vcKeys.map((vcKey, index) => {
if (vcKey.split(':')[4] === 'true') {
{vcMetadataOrderedByPinStatus.map((vcMetadata, index) => {
const iconProps = vcMetadata.isPinned ? pinIconProps : {};
return (
<VcItem
key={`${vcKey}-${index}`}
vcKey={vcKey}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
iconName="pushpin"
iconType="antdesign"
/>
);
}
})}
{controller.vcKeys.map((vcKey, index) => {
if (vcKey.split(':')[4] === 'false') {
return (
<VcItem
key={`${vcKey}-${index}`}
vcKey={vcKey}
{...iconProps}
key={`${vcMetadata.getVcKey()}-${index}`}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
/>
);
}
})}
</Column>
<Button
@@ -112,7 +106,7 @@ export const MyVcsTab: React.FC<HomeScreenTabProps> = props => {
/>
</React.Fragment>
)}
{controller.vcKeys.length === 0 && (
{controller.vcMetadatas.length === 0 && (
<React.Fragment>
<Column fill style={Theme.Styles.homeScreenContainer}>
<Image source={Theme.DigitalIdentityLogo} />

View File

@@ -4,7 +4,7 @@ import {ActorRefFrom} from 'xstate';
import {selectIsTampered} from '../../machines/store';
import {
selectIsRefreshingMyVcs,
selectMyVcs,
selectMyVcsMetadata,
VcEvents,
} from '../../machines/vc';
import {
@@ -40,7 +40,7 @@ export function useMyVcsTab(props: HomeScreenTabProps) {
AddVcModalService: useSelector(service, selectAddVcModal),
GetVcModalService: useSelector(service, selectGetVcModal),
vcKeys: useSelector(vcService, selectMyVcs),
vcMetadatas: useSelector(vcService, selectMyVcsMetadata),
isTampered: useSelector(storeService, selectIsTampered),
isRefreshingVcs: useSelector(vcService, selectIsRefreshingMyVcs),

View File

@@ -15,6 +15,7 @@ import {MY_VCS_STORE_KEY} from '../../shared/constants';
import {AddVcModalMachine} from './MyVcs/AddVcModalMachine';
import {GetVcModalMachine} from './MyVcs/GetVcModalMachine';
import Storage from '../../shared/storage';
import {VCMetadata} from '../../shared/VCMetadata';
const model = createModel(
{
@@ -163,7 +164,7 @@ export const MyVcsTabMachine = model.createMachine(
},
actions: {
refreshMyVc: send((_context, event) => VcEvents.REFRESH_MY_VCS(), {
refreshMyVc: send(_context => VcEvents.REFRESH_MY_VCS(), {
to: context => context.serviceRefs.vc,
}),
@@ -179,14 +180,14 @@ export const MyVcsTabMachine = model.createMachine(
(_context, event) => {
return StoreEvents.PREPEND(
MY_VCS_STORE_KEY,
(event as DoneInvokeEvent<string>).data,
(event as DoneInvokeEvent<VCMetadata>).data,
);
},
{to: context => context.serviceRefs.store},
),
sendVcAdded: send(
(_context, event) => VcEvents.VC_ADDED(event.response as string),
(_context, event) => VcEvents.VC_ADDED(event.response as VCMetadata),
{
to: context => context.serviceRefs.vc,
},

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, {useEffect} from 'react';
import {useTranslation} from 'react-i18next';
import {RefreshControl} from 'react-native';
import {Icon} from 'react-native-elements';
@@ -8,9 +8,9 @@ import { HomeScreenTabProps } from './HomeScreen';
import {useReceivedVcsTab} from './ReceivedVcsTabController';
import {VcItem} from '../../components/VcItem';
export const ReceivedVcsTab: React.FC<HomeScreenTabProps> = (props) => {
export const ReceivedVcsTab: React.FC<HomeScreenTabProps> = props => {
const {t} = useTranslation('ReceivedVcsTab');
const controller = useReceivedVcsTab(props);
const controller = useReceivedVcsTab();
return (
<Column fill style={{display: props.isVisible ? 'flex' : 'none'}}>
@@ -23,16 +23,16 @@ export const ReceivedVcsTab: React.FC<HomeScreenTabProps> = (props) => {
onRefresh={controller.REFRESH}
/>
}>
{controller.vcKeys.map((vcKey) => (
{controller.receivedVcsMetadata.map(vcMetadata => (
<VcItem
key={vcKey}
vcKey={vcKey}
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.VIEW_VC}
activeTab={props.service.id}
/>
))}
{controller.vcKeys.length === 0 && (
{controller.receivedVcsMetadata.length === 0 && (
<React.Fragment>
<Centered fill>
<Icon

View File

@@ -4,7 +4,7 @@ import { ActorRefFrom } from 'xstate';
import {
VcEvents,
selectIsRefreshingReceivedVcs,
selectReceivedVcs,
selectReceivedVcsMetadata,
} from '../../machines/vc';
import { vcItemMachine } from '../../machines/vcItem';
import { GlobalContext } from '../../shared/GlobalContext';
@@ -48,7 +48,7 @@ export function useReceivedVcsTab() {
return {
isVisible,
vcKeys: useSelector(vcService, selectReceivedVcs),
receivedVcsMetadata: useSelector(vcService, selectReceivedVcsMetadata),
isRefreshingVcs: useSelector(vcService, selectIsRefreshingReceivedVcs),

View File

@@ -18,7 +18,7 @@ const model = createModel(
STORE_RESPONSE: (response?: unknown) => ({ response }),
STORE_ERROR: (error: Error) => ({ error }),
ERROR: (error: Error) => ({ error }),
GET_RECEIVED_VCS_RESPONSE: (vcKeys: string[]) => ({ vcKeys }),
GET_RECEIVED_VCS_RESPONSE: (vcMetadatas: string[]) => ({ vcMetadatas }),
},
}
);

View File

@@ -42,7 +42,7 @@ const LanguageSetting: React.FC = () => {
);
};
export const ProfileScreen: React.FC<MainRouteProps> = (props) => {
export const ProfileScreen: React.FC<MainRouteProps> = props => {
const {t} = useTranslation('ProfileScreen');
const controller = useProfileScreen(props);

View File

@@ -40,7 +40,7 @@ export function useProfileScreen({ navigation }: MainRouteProps) {
const errorMsgBio: string = useSelector(bioService, selectError);
const unEnrolledNoticeBio: string = useSelector(
bioService,
selectUnenrolledNotice
selectUnenrolledNotice,
);
const {t} = useTranslation('AuthScreen');
@@ -101,29 +101,29 @@ export function useProfileScreen({ navigation }: MainRouteProps) {
credentialRegistry: useSelector(settingsService, selectCredentialRegistry),
credentialRegistryResponse: useSelector(
settingsService,
selectCredentialRegistryResponse
selectCredentialRegistryResponse,
),
isBiometricUnlockEnabled: useSelector(
settingsService,
selectBiometricUnlockEnabled
selectBiometricUnlockEnabled,
),
canUseBiometrics: useSelector(authService, selectCanUseBiometrics),
useBiometrics,
UPDATE_NAME: (names) =>
UPDATE_NAME: names =>
settingsService.send(SettingsEvents.UPDATE_NAME(names[0].value)),
UPDATE_VC_LABEL: (labels) =>
UPDATE_VC_LABEL: labels =>
settingsService.send(SettingsEvents.UPDATE_VC_LABEL(labels[0].value)),
UPDATE_CREDENTIAL_REGISTRY: (items) =>
UPDATE_CREDENTIAL_REGISTRY: items =>
settingsService.send(SettingsEvents.UPDATE_MIMOTO_HOST(items[0].value)),
UPDATE_CREDENTIAL_REGISTRY_RESPONSE: (credentialRegistryResponse: string) =>
settingsService.send(
SettingsEvents.UPDATE_CREDENTIAL_REGISTRY_RESPONSE(
credentialRegistryResponse
)
credentialRegistryResponse,
),
),
TOGGLE_BIOMETRIC: (enable: boolean) =>

View File

@@ -24,25 +24,27 @@ export const MyBindedVcs: React.FC<MyBindedVcsProps> = (props) => {
<React.Fragment>
<Column fill style={{ display: props.isVisible ? 'flex' : 'none' }}>
<Column fill>
{controller.vcKeys.length > 0 && (
{controller.shareableVcsMetadata.length > 0 && (
<>
<Column
fill
backgroundColor={Theme.Colors.lightGreyBackgroundColor}>
<Column padding="16 0" scroll>
<Column pX={14}>
{controller.vcKeys.length > 0 &&
controller.vcKeys.map((vcKey, index) => (
{controller.shareableVcsMetadata.length > 0 &&
controller.shareableVcsMetadata.map(
(vcMetadata, index) => (
<VcItem
key={vcKey}
vcKey={vcKey}
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.SELECT_VC_ITEM(index)}
showOnlyBindedVc
selectable
selected={index === controller.selectedIndex}
/>
))}
)
)}
</Column>
</Column>
</Column>
@@ -64,7 +66,7 @@ export const MyBindedVcs: React.FC<MyBindedVcsProps> = (props) => {
</Column>
</>
)}
{controller.vcKeys.length === 0 && (
{controller.shareableVcsMetadata.length === 0 && (
<React.Fragment>
<Centered fill>
<Text weight="semibold" margin="0 0 8 0">

View File

@@ -24,7 +24,7 @@ import {
selectIsSendingAuthenticate,
selectEssentialClaims,
} from '../../machines/QrLoginMachine';
import { selectBindedVcs } from '../../machines/vc';
import { selectBindedVcsMetadata } from '../../machines/vc';
import { vcItemMachine } from '../../machines/vcItem';
import { GlobalContext } from '../../shared/GlobalContext';
import { VC } from '../../types/vc';
@@ -51,7 +51,7 @@ export function useQrLogin({ service }: QrLoginProps) {
SELECT_VC(vcData);
},
vcKeys: useSelector(vcService, selectBindedVcs),
shareableVcsMetadata: useSelector(vcService, selectBindedVcsMetadata),
selectedVc: useSelector(service, selectSelectedVc),
linkTransactionResponse: useSelector(
service,

View File

@@ -2,7 +2,7 @@ import { useSelector } from '@xstate/react';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { selectShareableVcs } from '../../machines/vc';
import { selectShareableVcsMetadata } from '../../machines/vc';
import { GlobalContext } from '../../shared/GlobalContext';
import {
selectIsLocationDenied,
@@ -30,7 +30,10 @@ export function useScanScreen() {
const scanService = appService.children.get('scan');
const vcService = appService.children.get('vc');
const shareableVcs = useSelector(vcService, selectShareableVcs);
const shareableVcsMetadata = useSelector(
vcService,
selectShareableVcsMetadata
);
const isLocationDisabled = useSelector(scanService, selectIsLocationDisabled);
const isLocationDenied = useSelector(scanService, selectIsLocationDenied);
@@ -67,7 +70,7 @@ export function useScanScreen() {
return {
locationError,
isEmpty: !shareableVcs.length,
isEmpty: !shareableVcsMetadata.length,
isBluetoothPermissionDenied,
isNearByDevicesPermissionDenied,
isLocationDisabled,

View File

@@ -29,10 +29,10 @@ export const SelectVcOverlay: React.FC<SelectVcOverlayProps> = (props) => {
{t('chooseVc')} <Text weight="semibold">{props.receiverName}</Text>
</Text>
<Column margin="0 0 32 0" scroll>
{props.vcKeys.map((vcKey, index) => (
{props.vcMetadatas.map((vcMetadata, index) => (
<VcItem
key={`${vcKey}-${index}`}
vcKey={vcKey}
key={`${vcMetadata.getVcKey()}-${index}`}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.selectVcItem(index)}
selectable

View File

@@ -2,6 +2,7 @@ import { useState } from 'react';
import { ActorRefFrom } from 'xstate';
import { vcItemMachine } from '../../machines/vcItem';
import { VC } from '../../types/vc';
import { VCMetadata } from '../../shared/VCMetadata';
export function useSelectVcOverlay(props: SelectVcOverlayProps) {
const [selectedIndex, setSelectedIndex] = useState<number>(null);
@@ -34,7 +35,7 @@ export function useSelectVcOverlay(props: SelectVcOverlayProps) {
export interface SelectVcOverlayProps {
isVisible: boolean;
receiverName: string;
vcKeys: string[];
vcMetadatas: VCMetadata[];
onSelect: (vc: VC) => void;
onVerifyAndSelect: (vc: VC) => void;
onCancel: () => void;

View File

@@ -19,11 +19,11 @@ export const SendVcScreen: React.FC = () => {
const controller = useSendVcScreen();
let service;
if (controller.vcKeys?.length > 0) {
if (controller.shareableVcsMetadata?.length > 0) {
const firstVCMachine = useRef(
createVcItemMachine(
appService.getSnapshot().context.serviceRefs,
controller.vcKeys[0]
controller.shareableVcsMetadata[0]
)
);
@@ -78,10 +78,10 @@ export const SendVcScreen: React.FC = () => {
</Text>
</Column>
<Column scroll>
{controller.vcKeys.map((vcKey, index) => (
{controller.shareableVcsMetadata.map((vcMetadata, index) => (
<VcItem
key={vcKey}
vcKey={vcKey}
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.SELECT_VC_ITEM(index)}
selectable

View File

@@ -1,7 +1,7 @@
import { useSelector } from '@xstate/react';
import { useContext, useState } from 'react';
import { ActorRefFrom } from 'xstate';
import { selectShareableVcs } from '../../machines/vc';
import { selectShareableVcsMetadata } from '../../machines/vc';
import { vcItemMachine } from '../../machines/vcItem';
import { GlobalContext } from '../../shared/GlobalContext';
import {
@@ -41,7 +41,7 @@ export function useSendVcScreen() {
receiverInfo: useSelector(scanService, selectReceiverInfo),
reason: useSelector(scanService, selectReason),
vcName: useSelector(scanService, selectVcName),
vcKeys: useSelector(vcService, selectShareableVcs),
shareableVcsMetadata: useSelector(vcService, selectShareableVcsMetadata),
selectedVc: useSelector(scanService, selectSelectedVc),
isSelectingVc: useSelector(scanService, selectIsSelectingVc),

View File

@@ -12,7 +12,6 @@ import {CopyButton} from '../../components/CopyButton';
import testIDProps from '../../shared/commonUtil';
import {__InjiVersion, __TuvaliVersion} from '../../shared/GlobalVariables';
export const AboutInji: React.FC<AboutInjiProps> = ({appId}) => {
const {t} = useTranslation('AboutInji');

View File

@@ -9,7 +9,7 @@ import { Theme } from '../../components/ui/styleUtils';
import appMetaData from '../../AppMetaData.md';
import {__InjiVersion, __TuvaliVersion} from '../../shared/GlobalVariables';
export const AppMetaData: React.FC<AppMetaDataProps> = (props) => {
export const AppMetaData: React.FC<AppMetaDataProps> = props => {
const {t} = useTranslation('AppMetaData');
const [isViewing, setIsViewing] = useState(false);

View File

@@ -30,16 +30,16 @@ export const ReceivedCardsModal: React.FC<ReceivedCardsProps> = ({
onRefresh={controller.REFRESH}
/>
}>
{controller.vcKeys.map(vcKey => (
{controller.receivedVcsMetadata.map(vcMetadata => (
<VcItem
key={vcKey}
vcKey={vcKey}
key={vcMetadata.getVcKey()}
vcMetadata={vcMetadata}
margin="0 2 8 2"
isSharingVc
onPress={controller.VIEW_VC}
/>
))}
{controller.vcKeys.length === 0 && (
{controller.receivedVcsMetadata.length === 0 && (
<React.Fragment>
<Centered fill>
<Icon

View File

@@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next';
import {useRevoke} from './RevokeController';
// Intentionally hidden using {display:'none'} - Refer mosip/inji/issue#607
export const Revoke: React.FC<RevokeScreenProps> = (props) => {
export const Revoke: React.FC<RevokeScreenProps> = props => {
const controller = useRevoke();
const {t} = useTranslation('ProfileScreen');
@@ -66,7 +66,7 @@ export const Revoke: React.FC<RevokeScreenProps> = (props) => {
<Divider />
<Row style={Theme.RevokeStyles.rowStyle} fill>
<View style={Theme.RevokeStyles.revokeView}>
{controller.vidKeys.length > 0 && (
{controller.uniqueVidsMetadata.length > 0 && (
<Column
scroll
refreshControl={
@@ -75,19 +75,26 @@ export const Revoke: React.FC<RevokeScreenProps> = (props) => {
onRefresh={controller.REFRESH}
/>
}>
{controller.vidKeys.map((vcKey, index) => (
{controller.uniqueVidsMetadata.map((vcMetadata, index) => {
return (
<VidItem
key={`${vcKey}-${index}`}
vcKey={vcKey}
key={`${vcMetadata.getVcKey()}-${index}`}
vcMetadata={vcMetadata}
margin="0 2 8 2"
onPress={controller.selectVcItem(index, vcKey)}
onPress={controller.selectVcItem(
index,
vcMetadata.getVcKey(),
)}
selectable
selected={controller.selectedVidKeys.includes(vcKey)}
selected={controller.selectedVidUniqueIds.includes(
vcMetadata.getVcKey(),
)}
/>
))}
);
})}
</Column>
)}
{controller.vidKeys.length === 0 && (
{controller.uniqueVidsMetadata.length === 0 && (
<React.Fragment>
<Centered fill>
<Text weight="semibold" margin="0 0 8 0">
@@ -99,7 +106,7 @@ export const Revoke: React.FC<RevokeScreenProps> = (props) => {
</View>
<Column margin="0 20">
<Button
disabled={controller.selectedVidKeys.length === 0}
disabled={controller.selectedVidUniqueIds.length === 0}
title={t('revokeHeader')}
onPress={controller.CONFIRM_REVOKE_VC}
/>
@@ -118,16 +125,21 @@ export const Revoke: React.FC<RevokeScreenProps> = (props) => {
</Text>
<Text margin="0 0 12 0">
{t('revokingVids', {
count: controller.selectedVidKeys.length,
count: controller.selectedVidUniqueIds.length,
})}
</Text>
{controller.selectedVidKeys.map((vcKey, index) => (
{controller.selectedVidUniqueIds.map((uniqueId, index) => (
<View style={Theme.RevokeStyles.flexRow} key={index}>
<Text margin="0 8" weight="bold">
{'\u2022'}
</Text>
<Text margin="0 0 0 0" weight="bold">
{vcKey.split(':')[2]}
{/*TODO: Change this to UIN? and Optimize*/}
{
controller.uniqueVidsMetadata.find(
metadata => metadata.getVcKey() === uniqueId,
)?.id
}
</Text>
</View>
))}

View File

@@ -4,7 +4,7 @@ import NetInfo from '@react-native-community/netinfo';
import { GlobalContext } from '../../shared/GlobalContext';
import {
selectIsRefreshingMyVcs,
selectMyVcs,
selectMyVcsMetadata,
VcEvents,
} from '../../machines/vc';
import { vcItemMachine } from '../../machines/vcItem';
@@ -24,7 +24,7 @@ export function useRevoke() {
const { appService } = useContext(GlobalContext);
const vcService = appService.children.get('vc');
const revokeService = appService.children.get('RevokeVids');
const vcKeys = useSelector(vcService, selectMyVcs);
const vcsMetadata = useSelector(vcService, selectMyVcsMetadata);
const isRevokingVc = useSelector(revokeService, selectIsRevokingVc);
const isLoggingRevoke = useSelector(revokeService, selectIsLoggingRevoke);
const isAcceptingOtpInput = useSelector(
@@ -38,20 +38,23 @@ export function useRevoke() {
const [toastVisible, setToastVisible] = useState(false);
const [message, setMessage] = useState('');
const [selectedIndex, setSelectedIndex] = useState<number>(null);
const [selectedVidKeys, setSelectedVidKeys] = useState<string[]>([]);
const [selectedVidUniqueIds, setSelectedVidUniqueIds] = useState<string[]>(
[]
);
const vidKeys = vcKeys.filter((vc) => {
const vcKey = vc.split(':');
return vcKey[1] === 'VID';
});
const vidsMetadata = vcsMetadata.filter(
(vcMetadata) => vcMetadata.idType === 'VID'
);
const selectVcItem = (index: number, vcKey: string) => {
const selectVcItem = (index: number, vcUniqueId: string) => {
return () => {
setSelectedIndex(index);
if (selectedVidKeys.includes(vcKey)) {
setSelectedVidKeys(selectedVidKeys.filter((item) => item !== vcKey));
if (selectedVidUniqueIds.includes(vcUniqueId)) {
setSelectedVidUniqueIds(
selectedVidUniqueIds.filter((item) => item !== vcUniqueId)
);
} else {
setSelectedVidKeys((prevArray) => [...prevArray, vcKey]);
setSelectedVidUniqueIds((prevArray) => [...prevArray, vcUniqueId]);
}
};
};
@@ -67,7 +70,7 @@ export function useRevoke() {
useEffect(() => {
if (isRevokingVc) {
setSelectedVidKeys([]);
setSelectedVidUniqueIds([]);
showToast(t('revokeSuccessful'));
}
if (isLoggingRevoke) {
@@ -85,10 +88,10 @@ export function useRevoke() {
isViewing,
message,
selectedIndex,
selectedVidKeys,
selectedVidUniqueIds,
toastVisible,
vidKeys: vidKeys.filter(
(vcKey, index, vid) => vid.indexOf(vcKey) === index
uniqueVidsMetadata: vidsMetadata.filter(
(vcMetadata, index, vid) => vid.indexOf(vcMetadata) === index
),
CONFIRM_REVOKE_VC: () => {
@@ -101,7 +104,7 @@ export function useRevoke() {
revokeService.send(RevokeVidsEvents.INPUT_OTP(otp)),
REFRESH: () => vcService.send(VcEvents.REFRESH_MY_VCS()),
REVOKE_VC: () => {
revokeService.send(RevokeVidsEvents.REVOKE_VCS(selectedVidKeys));
revokeService.send(RevokeVidsEvents.REVOKE_VCS(selectedVidUniqueIds));
setRevoking(false);
//since nested modals/overlays don't work in ios, we need to toggle revoke screen
setIsViewing(false);

View File

@@ -54,7 +54,7 @@ export const SettingScreen: React.FC<
const {t} = useTranslation('SettingScreen');
const controller = useSettingsScreen(props);
const updateRegistry = (items) => {
const updateRegistry = items => {
controller.UPDATE_CREDENTIAL_REGISTRY(items[0].value, items[1].value);
};

View File

@@ -131,11 +131,11 @@ export function useSettingsScreen(props: RootRouteProps & RequestRouteProps) {
UPDATE_CREDENTIAL_REGISTRY: (
credentialRegistry: string,
esignetHostUrl: string
esignetHostUrl: string,
) => {
settingsService.send(SettingsEvents.UPDATE_ESIGNET_HOST(esignetHostUrl)),
settingsService.send(
SettingsEvents.UPDATE_MIMOTO_HOST(credentialRegistry)
SettingsEvents.UPDATE_MIMOTO_HOST(credentialRegistry),
);
},

View File

@@ -7,7 +7,7 @@ const dependencies = require('../package-lock.json').dependencies;
function getTuvaliPackageDetails() {
let packageVersion, packageCommitId;
Object.keys(dependencies).forEach((dependencyName) => {
Object.keys(dependencies).forEach(dependencyName => {
const dependencyData = dependencies[dependencyName];
if (dependencyName == 'react-native-tuvali') {

55
shared/VCMetadata.ts Normal file
View File

@@ -0,0 +1,55 @@
import {VC, VcIdType} from '../types/vc';
const VC_KEY_PREFIX = 'VC';
const VC_ITEM_STORE_KEY_REGEX = '^VC_[a-z0-9-]+$';
export class VCMetadata {
idType: VcIdType | string = '';
requestId = '';
isPinned = false;
id: string = '';
static vcKeyRegExp = new RegExp(VC_ITEM_STORE_KEY_REGEX);
constructor({idType = '', requestId = '', isPinned = false, id = ''} = {}) {
this.idType = idType;
this.requestId = requestId;
this.isPinned = isPinned;
this.id = id;
}
static fromVC(vc: Partial<VC>) {
return new VCMetadata({
idType: vc.idType,
requestId: vc.requestId,
isPinned: vc.isPinned || false,
id: vc.id,
});
}
static fromVcMetadataString(vcMetadataStr: string) {
try {
return new VCMetadata(JSON.parse(vcMetadataStr));
} catch (e) {
console.error('Failed to parse VC Metadata', e);
return new VCMetadata();
}
}
static isVCKey(key: string): boolean {
return VCMetadata.vcKeyRegExp.exec(key) != null;
}
// Used for mmkv storage purposes and as a key for components and vc maps
// Update VC_ITEM_STORE_KEY_REGEX in case of changes in vckey
getVcKey(): string {
return `${VC_KEY_PREFIX}_${this.requestId}`;
}
equals(other: VCMetadata): boolean {
return this.getVcKey() === other.getVcKey();
}
}
export function parseMetadatas(metadataStrings: object[]) {
return metadataStrings.map(o => new VCMetadata(o));
}

View File

@@ -4,7 +4,7 @@ import argon2 from 'react-native-argon2';
export const hashData = async (
data: string,
salt: string,
config: Argon2iConfig
config: Argon2iConfig,
): Promise<string> => {
const result = await argon2(data, salt, config);
return result.rawHash as string;

View File

@@ -1,5 +1,4 @@
import {Platform} from 'react-native';
import { VC } from '../types/vc';
import {
MIMOTO_HOST,
ESIGNET_HOST,
@@ -10,8 +9,8 @@ import { Argon2iConfig } from './commonUtil';
export let MIMOTO_BASE_URL = MIMOTO_HOST;
export let ESIGNET_BASE_URL = ESIGNET_HOST;
export const changeCrendetialRegistry = (host) => (MIMOTO_BASE_URL = host);
export const changeEsignetUrl = (host) => (ESIGNET_BASE_URL = host);
export const changeCrendetialRegistry = host => (MIMOTO_BASE_URL = host);
export const changeEsignetUrl = host => (ESIGNET_BASE_URL = host);
export const MY_VCS_STORE_KEY = 'myVCs';
@@ -19,23 +18,6 @@ export const RECEIVED_VCS_STORE_KEY = 'receivedVCs';
export const MY_LOGIN_STORE_KEY = 'myLogins';
export const VC_ITEM_STORE_KEY = (vc: Partial<VC>) =>
`vc:${vc.idType}:${vc.hashedId}:${vc.requestId}:${vc.isPinned}:${vc.id}`;
export const VC_ITEM_STORE_KEY_AFTER_DOWNLOAD = (vc: Partial<VC>) =>
`vc:${vc.idType}:${vc.hashedId}:${vc.requestId}:${vc.isPinned}`;
//Regex expression to evaluate if the key is for a VC
export const VC_ITEM_STORE_KEY_REGEX =
'^vc:(UIN|VID):[a-z0-9]+:[a-z0-9-]+:[true|false]+(:[0-9-]+)?$';
//To compare the vckey with requestId, when the vc is pinned
export const isSameVC = (vcKey: string, pinnedVcKey: string) => {
const requestId = vcKey.split(':')[3];
const pinnedRequestId = pinnedVcKey.split(':')[3];
return requestId === pinnedRequestId;
};
export let individualId = '';
export const GET_INDIVIDUAL_ID = (ind_Id: string) => {

14
shared/javascript.ts Normal file
View File

@@ -0,0 +1,14 @@
export function groupBy<T>(array: T[], predicate: (T) => boolean) {
const trueElements = [];
const falseElements = [];
array?.forEach((e) => {
if (predicate(e)) {
trueElements.push(e);
} else {
falseElements.push(e);
}
});
return [trueElements, falseElements];
}

View File

@@ -13,7 +13,7 @@ export async function request(
method: 'GET' | 'POST' | 'PATCH',
path: `/${string}`,
body?: Record<string, unknown>,
host= MIMOTO_BASE_URL
host = MIMOTO_BASE_URL,
) {
const headers = {
'Content-Type': 'application/json',
@@ -36,7 +36,7 @@ export async function request(
'The backend API ' +
backendUrl +
' returned error code 400 with message --> ' +
errorMessage
errorMessage,
);
throw new Error(errorMessage);
}
@@ -50,7 +50,7 @@ export async function request(
' returned error response --> error code is : ' +
errorCode +
' error message is : ' +
errorMessage
errorMessage,
);
throw new BackendResponseError(errorCode, errorMessage);
}

View File

@@ -1,5 +1,4 @@
import {MMKVLoader} from 'react-native-mmkv-storage';
import { VC_ITEM_STORE_KEY_REGEX } from './constants';
import CryptoJS from 'crypto-js';
import {
DocumentDirectoryPath,
@@ -23,14 +22,14 @@ import {
HMAC_ALIAS,
isCustomSecureKeystore,
} from './cryptoutil/cryptoUtil';
import {VCMetadata} from './VCMetadata';
const MMKV = new MMKVLoader().initialize();
const vcKeyRegExp = new RegExp(VC_ITEM_STORE_KEY_REGEX);
const vcDirectoryPath = `${DocumentDirectoryPath}/inji/VC`;
async function generateHmac(
encryptionKey: string,
data: string
data: string,
): Promise<string> {
if (!isCustomSecureKeystore()) {
return CryptoJS.HmacSHA256(encryptionKey, data).toString();
@@ -51,10 +50,10 @@ class Storage {
static setItem = async (
key: string,
data: string,
encryptionKey?: string
encryptionKey?: string,
) => {
try {
const isSavingVC = vcKeyRegExp.exec(key);
const isSavingVC = VCMetadata.isVCKey(key);
if (isSavingVC) {
await this.storeVcHmac(encryptionKey, data, key);
return await this.storeVC(key, data);
@@ -69,9 +68,9 @@ class Storage {
static getItem = async (key: string, encryptionKey?: string) => {
try {
const isSavingVC = vcKeyRegExp.exec(key);
const isVCKey = VCMetadata.isVCKey(key);
if (isSavingVC) {
if (isVCKey) {
const data = await this.readVCFromFile(key);
const isCorrupted = await this.isCorruptedVC(key, encryptionKey, data);
@@ -88,7 +87,7 @@ class Storage {
private static async isCorruptedVC(
key: string,
encryptionKey: string,
data: string
data: string,
) {
const storedHMACofCurrentVC = await this.readHmacForVC(key, encryptionKey);
const HMACofVC = await generateHmac(encryptionKey, data);
@@ -114,7 +113,7 @@ class Storage {
private static async storeVcHmac(
encryptionKey: string,
data: string,
key: string
key: string,
) {
const HMACofVC = await generateHmac(encryptionKey, data);
const encryptedHMACofVC = await encryptJson(encryptionKey, HMACofVC);
@@ -122,7 +121,7 @@ class Storage {
}
static removeItem = async (key: string) => {
if (vcKeyRegExp.exec(key)) {
if (VCMetadata.isVCKey(key)) {
const path = getFilePath(key);
return await unlink(path);
}
@@ -156,6 +155,7 @@ class Storage {
return freeDiskStorageInBytes <= minimumStorageLimitInBytes;
};
}
/**
* The VC file name will not have the pinned / unpinned state, we will splice the state as this will change.
* replace ':' with '_' in the key to get the file name as ':' are not allowed in filenames
@@ -171,8 +171,7 @@ const getFileName = (key: string) => {
* These paths are coming from DocumentDirectoryPath in react-native-fs.
*/
const getFilePath = (key: string) => {
const fileName = getFileName(key);
return `${vcDirectoryPath}/${fileName}.txt`;
return `${vcDirectoryPath}/${key}.txt`;
};
/**
@@ -180,12 +179,12 @@ const getFilePath = (key: string) => {
* eg: "vc:UIN:6732935275:e7426576-112f-466a-961a-1ed9635db628:true" is changed to "vc:UIN:6732935275:e7426576-112f-466a-961a-1ed9635db628"
*/
const getVCKeyName = (key: string) => {
return key.split(':').splice(0, 4).join(':');
return key;
};
// To print the MMKV data cal this function in getItem
const getMMKVData = async () => {
const mmkvData = await MMKV.indexer.getKeys();
return await MMKV.indexer.getKeys();
};
export default Storage;

View File

@@ -15,9 +15,8 @@ export interface VC {
reason?: VCSharingReason[];
shouldVerifyPresence?: boolean;
walletBindingResponse?: WalletBindingResponse;
credentialRegistry: string;
credentialRegistry?: string;
isPinned?: boolean;
hashedId: string;
}
export interface VCSharingReason {
@@ -61,25 +60,25 @@ type VCContext = (string | Record<string, unknown>)[];
export interface VerifiableCredential {
'@context': VCContext;
'credentialSubject': CredentialSubject;
'id': string;
'issuanceDate': string;
'issuer': string;
'proof': {
credentialSubject: CredentialSubject;
id: string;
issuanceDate: string;
issuer: string;
proof: {
created: string;
jws: string;
proofPurpose: 'assertionMethod' | string;
type: 'RsaSignature2018' | string;
verificationMethod: string;
};
'type': VerifiableCredentialType[];
type: VerifiableCredentialType[];
}
export interface VerifiablePresentation {
'@context': VCContext;
'verifiableCredential': VerifiableCredential[];
'type': 'VerifiablePresentation';
'proof': {
verifiableCredential: VerifiableCredential[];
type: 'VerifiablePresentation';
proof: {
created: string;
jws: string;
proofPurpose: 'authentication' | string;