Merge remote-tracking branch 'idpass/develop' into develop-idpass-sync

This commit is contained in:
pmigueld
2022-10-17 17:39:17 +08:00
91 changed files with 3017 additions and 941 deletions

2
.env
View File

@@ -1,3 +1,3 @@
MIMOTO_HOST=https://api.qa4.mosip.net/residentmobileapp
#MIMOTO_HOST=http://mock.mimoto.newlogic.dev
GOOGLE_NEARBY_MESSAGES_API_KEY=

View File

@@ -14,6 +14,10 @@ export const DropdownIcon: React.FC<DropdownProps> = (props) => {
item.onPress();
};
const filteredItems = (idType: string, items: Item[]) => {
return items.filter((item) => !item.idType || item.idType === idType);
};
const renderItem = ({ item }) => {
return (
<View
@@ -51,7 +55,7 @@ export const DropdownIcon: React.FC<DropdownProps> = (props) => {
content={
<View>
<FlatList
data={props.items}
data={filteredItems(props.idType, props.items)}
renderItem={renderItem}
keyExtractor={(item, index) => index.toString()}
/>
@@ -63,11 +67,13 @@ export const DropdownIcon: React.FC<DropdownProps> = (props) => {
);
};
interface Item {
idType?: string;
label: string;
onPress?: () => void;
}
interface DropdownProps {
idType: string;
icon: string;
items: Item[];
}

87
components/Message.tsx Normal file
View File

@@ -0,0 +1,87 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet, View } from 'react-native';
import { LinearProgress } from 'react-native-elements';
import { Button, Centered, Column, Text } from './ui';
import { Colors, elevation } from './ui/styleUtils';
const styles = StyleSheet.create({
overlay: {
...elevation(5),
backgroundColor: Colors.White,
padding: 0,
},
button: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
viewContainer: {
backgroundColor: 'rgba(0,0,0,.6)',
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
position: 'absolute',
top: 0,
zIndex: 9,
},
boxContainer: {
backgroundColor: Colors.White,
padding: 24,
elevation: 6,
borderRadius: 4,
},
});
export const Message: React.FC<MessageProps> = (props) => {
const { t } = useTranslation('common');
return (
<View style={styles.viewContainer} onTouchStart={props.onBackdropPress}>
<Centered fill>
<Column
width={Dimensions.get('screen').width * 0.8}
style={styles.boxContainer}>
<Column>
{props.title && (
<Text weight="semibold" margin="0 0 12 0">
{props.title}
</Text>
)}
{props.message && <Text margin="0 0 12 0">{props.message}</Text>}
{props.progress && <Progress progress={props.progress} />}
{props.hint && (
<Text size="smaller" color={Colors.Grey} margin={[4, 0, 0, 0]}>
{props.hint}
</Text>
)}
</Column>
{props.onCancel && (
<Button
title={t('cancel')}
onPress={props.onCancel}
styles={styles.button}
/>
)}
</Column>
</Centered>
</View>
);
};
const Progress: React.FC<Pick<MessageProps, 'progress'>> = (props) => {
return typeof props.progress === 'boolean' ? (
props.progress && (
<LinearProgress variant="indeterminate" color={Colors.Orange} />
)
) : (
<LinearProgress variant="determinate" value={props.progress} />
);
};
export interface MessageProps {
title?: string;
message?: string;
progress?: boolean | number;
hint?: string;
onCancel?: () => void;
onBackdropPress?: () => void;
}

View File

@@ -1,46 +1,79 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet } from 'react-native';
import { Overlay, LinearProgress } from 'react-native-elements';
import { Column, Text } from './ui';
import { Button, Column, Text } from './ui';
import { Theme } from './ui/styleUtils';
const styles = StyleSheet.create({
overlay: {
...Theme.elevation(5),
backgroundColor: Theme.Colors.whiteBackgroundColor,
padding: 0,
},
button: {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
});
export const MessageOverlay: React.FC<MessageOverlayProps> = (props) => {
const { t } = useTranslation('common');
return (
<Overlay
isVisible={props.isVisible}
overlayStyle={styles.overlay}
onBackdropPress={props.onBackdropPress}
onShow={props.onShow}>
<Column padding="24" width={Dimensions.get('screen').width * 0.8}>
{props.title && (
<Text weight="semibold" margin="0 0 12 0">
{props.title}
</Text>
)}
{props.message && <Text margin="0 0 12 0">{props.message}</Text>}
{props.hasProgress && (
<LinearProgress
variant="indeterminate"
color={Theme.Colors.Loading}
onShow={props.onShow}
onBackdropPress={props.onBackdropPress}>
<Column width={Dimensions.get('screen').width * 0.8}>
<Column padding="24">
{props.title && (
<Text weight="semibold" margin="0 0 12 0">
{props.title}
</Text>
)}
{props.message && <Text margin="0 0 12 0">{props.message}</Text>}
{props.progress && <Progress progress={props.progress} />}
{props.hint && (
<Text
size="smaller"
color={Theme.Colors.textLabel}
margin={[4, 0, 0, 0]}>
{props.hint}
</Text>
)}
{props.children}
</Column>
{!props.children && props.onCancel ? (
<Button
title={t('cancel')}
onPress={props.onCancel}
styles={styles.button}
/>
)}
) : null}
</Column>
</Overlay>
);
};
interface MessageOverlayProps {
const Progress: React.FC<Pick<MessageOverlayProps, 'progress'>> = (props) => {
return typeof props.progress === 'boolean' ? (
props.progress && (
<LinearProgress variant="indeterminate" color={Theme.Colors.Loading} />
)
) : (
<LinearProgress variant="determinate" value={props.progress} />
);
};
export interface MessageOverlayProps {
isVisible: boolean;
title?: string;
message?: string;
hasProgress?: boolean;
progress?: boolean | number;
hint?: string;
onCancel?: () => void;
onBackdropPress?: () => void;
onShow?: () => void;
}

View File

@@ -0,0 +1,5 @@
{
"title": "OIDC Authentication",
"text": "To be replaced with the OIDC provider UI",
"verify": "Verify"
}

58
components/OIDcAuth.tsx Normal file
View File

@@ -0,0 +1,58 @@
import React from 'react';
import { Dimensions, StyleSheet, View } from 'react-native';
import { useTranslation } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { Button, Centered, Column, Text } from './ui';
import { ModalProps } from './ui/Modal';
import { Colors } from './ui/styleUtils';
const styles = StyleSheet.create({
viewContainer: {
backgroundColor: Colors.White,
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
position: 'absolute',
top: 0,
zIndex: 9,
padding: 32,
},
});
export const OIDcAuthenticationModal: React.FC<OIDcAuthenticationModalProps> = (
props
) => {
const { t } = useTranslation('OIDcAuth');
return (
<View style={styles.viewContainer}>
<Column safe fill align="space-between">
<Centered fill>
<Icon
name="card-account-details-outline"
color={Colors.Orange}
size={30}
/>
<Text
align="center"
weight="bold"
margin="8 0 12 0"
style={{ fontSize: 24 }}>
{t('title')}
</Text>
<Text align="center">{t('text')}</Text>
<Text align="center" color={Colors.Red} margin="16 0 0 0">
{props.error}
</Text>
</Centered>
<Column>
<Button title={t('verify')} onPress={() => props.onVerify()} />
</Column>
</Column>
</View>
);
};
interface OIDcAuthenticationModalProps extends ModalProps {
onVerify: () => void;
error?: string;
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { Button, Centered, Column, Text } from './ui';
import { Modal, ModalProps } from './ui/Modal';
import { Colors } from './ui/styleUtils';
export const OIDcAuthenticationOverlay: React.FC<
OIDcAuthenticationModalProps
> = (props) => {
const { t } = useTranslation('OIDcAuth');
return (
<Modal isVisible={props.isVisible} onDismiss={props.onDismiss}>
<Column fill padding="32" align="space-between">
<Centered fill>
<Icon
name="card-account-details-outline"
color={Colors.Orange}
size={30}
/>
<Text
align="center"
weight="bold"
margin="8 0 12 0"
style={{ fontSize: 24 }}>
{t('title')}
</Text>
<Text align="center">{t('text')}</Text>
<Text align="center" color={Colors.Red} margin="16 0 0 0">
{props.error}
</Text>
</Centered>
<Column>
<Button title={t('verify')} onPress={() => props.onVerify()} />
</Column>
</Column>
</Modal>
);
};
interface OIDcAuthenticationModalProps extends ModalProps {
onVerify: () => void;
error?: string;
}

View File

@@ -46,7 +46,7 @@ export const QrScanner: React.FC<QrScannerProps> = (props) => {
if (hasPermission === false) {
return (
<Column fill align="space-between">
<Text align="center" color={Theme.Colors.errorMessage}>
<Text align="center" margin="16 0" color={Theme.Colors.errorMessage}>
{t('missingPermissionText')}
</Text>
<Button title={t('allowCameraButton')} onPress={openSettings} />

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet, View } from 'react-native';
import { Button, Centered, Column, Row, Text } from './ui';
import { Colors } from './ui/styleUtils';
const styles = StyleSheet.create({
viewContainer: {
backgroundColor: 'rgba(0,0,0,.6)',
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
position: 'absolute',
top: 0,
zIndex: 999999,
},
boxContainer: {
backgroundColor: Colors.White,
padding: 24,
elevation: 6,
borderRadius: 4,
},
});
export const RevokeConfirmModal: React.FC<RevokeConfirmModalProps> = (
props
) => {
const { t } = useTranslation('ViewVcModal');
return (
<View style={styles.viewContainer}>
<Centered fill>
<Column
width={Dimensions.get('screen').width * 0.8}
style={styles.boxContainer}>
<Text weight="semibold" margin="0 0 12 0">
{t('revoke')}
</Text>
<Text margin="0 0 12 0">{t('revoking', { vid: props.id })}</Text>
<Row>
<Button
fill
type="clear"
title={t('cancel')}
onPress={() => props.onCancel()}
/>
<Button fill title={t('revoke')} onPress={props.onRevoke} />
</Row>
</Column>
</Centered>
</View>
);
};
interface RevokeConfirmModalProps {
onCancel: () => void;
onRevoke: () => void;
id: string;
}

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { Overlay, Input } from 'react-native-elements';
import { Button, Column, Row, Text } from './ui';
import { Dimensions, StyleSheet, View } from 'react-native';
import { Input } from 'react-native-elements';
import { Button, Centered, Column, Row, Text } from './ui';
import { Theme } from './ui/styleUtils';
import { useTranslation } from 'react-i18next';
@@ -11,6 +11,20 @@ const styles = StyleSheet.create({
backgroundColor: Theme.Colors.overlayBackgroundColor,
padding: 0,
},
viewContainer: {
backgroundColor: 'rgba(0,0,0,.6)',
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
position: 'absolute',
top: 0,
zIndex: 9,
},
boxContainer: {
backgroundColor: Theme.Colors.whiteBackgroundColor,
padding: 24,
elevation: 6,
borderRadius: 4,
},
});
export const TextEditOverlay: React.FC<EditOverlayProps> = (props) => {
@@ -18,27 +32,32 @@ export const TextEditOverlay: React.FC<EditOverlayProps> = (props) => {
const [value, setValue] = useState(props.value);
return (
<Overlay
isVisible={props.isVisible}
overlayStyle={styles.overlay}
onBackdropPress={props.onDismiss}>
<Column pX={24} pY={24} width={Dimensions.get('screen').width * 0.8}>
<Text weight="semibold" margin="0 0 16 0">
{props.label}
</Text>
<Input autoFocus value={value} onChangeText={setValue} />
<Row>
<Button
fill
type="clear"
title={t('cancel')}
onPress={dismiss}
margin="0 8 0 0"
/>
<Button fill title={t('save')} onPress={() => props.onSave(value)} />
</Row>
</Column>
</Overlay>
<View style={styles.viewContainer}>
<Centered fill>
<Column
width={Dimensions.get('screen').width * 0.8}
style={styles.boxContainer}>
<Text weight="semibold" margin="0 0 16 0">
{props.label}
</Text>
<Input autoFocus value={value} onChangeText={setValue} />
<Row>
<Button
fill
type="clear"
title={t('cancel')}
onPress={dismiss}
margin="0 8 0 0"
/>
<Button
fill
title={t('save')}
onPress={() => props.onSave(value)}
/>
</Row>
</Column>
</Centered>
</View>
);
function dismiss() {

View File

@@ -4,7 +4,7 @@ import * as DateFnsLocale from '../lib/date-fns/locale';
import { useTranslation } from 'react-i18next';
import { Image, ImageBackground } from 'react-native';
import { Icon } from 'react-native-elements';
import { VC, CredentialSubject } from '../types/vc';
import { VC, CredentialSubject, LocalizedField } from '../types/vc';
import { Column, Row, Text } from './ui';
import { Theme } from './ui/styleUtils';
import { TextItem } from './ui/TextItem';
@@ -243,11 +243,6 @@ interface VcDetailsProps {
vc: VC;
}
interface LocalizedField {
language: string;
value: string;
}
function getFullAddress(credential: CredentialSubject) {
if (!credential) {
return '';
@@ -269,7 +264,7 @@ function getFullAddress(credential: CredentialSubject) {
.join(', ');
}
function getLocalizedField(rawField: string | LocalizedField) {
function getLocalizedField(rawField: string | LocalizedField[]) {
if (typeof rawField === 'string') {
return rawField;
}

134
components/VidItem.tsx Normal file
View File

@@ -0,0 +1,134 @@
import React, { useContext, useRef } from 'react';
import { useInterpret, useSelector } from '@xstate/react';
import { Pressable, StyleSheet } from 'react-native';
import { CheckBox } from 'react-native-elements';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
import { ActorRefFrom } from 'xstate';
import {
createVcItemMachine,
selectVerifiableCredential,
selectGeneratedOn,
selectTag,
selectId,
vcItemMachine,
} from '../machines/vcItem';
import { Column, Row, Text } from './ui';
import { Colors } from './ui/styleUtils';
import { RotatingIcon } from './RotatingIcon';
import { GlobalContext } from '../shared/GlobalContext';
const styles = StyleSheet.create({
title: {
color: Colors.Black,
backgroundColor: 'transparent',
},
loadingTitle: {
color: 'transparent',
backgroundColor: Colors.Grey5,
borderRadius: 4,
},
subtitle: {
backgroundColor: 'transparent',
},
loadingSubtitle: {
backgroundColor: Colors.Grey,
borderRadius: 4,
},
container: {
backgroundColor: Colors.White,
},
loadingContainer: {
backgroundColor: Colors.Grey6,
borderRadius: 4,
},
});
export const VidItem: React.FC<VcItemProps> = (props) => {
const { appService } = useContext(GlobalContext);
const machine = useRef(
createVcItemMachine(
appService.getSnapshot().context.serviceRefs,
props.vcKey
)
);
const service = useInterpret(machine.current);
const uin = useSelector(service, selectId);
const tag = useSelector(service, selectTag);
const verifiableCredential = useSelector(service, selectVerifiableCredential);
const generatedOn = useSelector(service, selectGeneratedOn);
const selectableOrCheck = props.selectable ? (
<CheckBox
checked={props.selected}
checkedIcon={<Icon name="checkbox-intermediate" size={24} />}
uncheckedIcon={<Icon name="checkbox-blank-outline" size={24} />}
onPress={() => props.onPress(service)}
/>
) : (
<Icon name="chevron-right" />
);
return (
<Pressable
onPress={() => props.onPress(service)}
disabled={!verifiableCredential}>
<Row
elevation={!verifiableCredential ? 0 : 2}
crossAlign="center"
margin={props.margin}
backgroundColor={!verifiableCredential ? Colors.Grey6 : Colors.White}
padding={[16, 16]}
style={
!verifiableCredential ? styles.loadingContainer : styles.container
}>
<Column fill margin="0 24 0 0">
<Text
weight="semibold"
style={!verifiableCredential ? styles.loadingTitle : styles.title}
margin="0 0 6 0">
{!verifiableCredential ? '' : tag || uin}
</Text>
<Text
size="smaller"
numLines={1}
style={
!verifiableCredential ? styles.loadingSubtitle : styles.subtitle
}>
{!verifiableCredential
? ''
: getLocalizedField(
verifiableCredential.credentialSubject.fullName
) +
' · ' +
generatedOn}
</Text>
</Column>
{verifiableCredential ? (
selectableOrCheck
) : (
<RotatingIcon name="sync" color={Colors.Grey5} />
)}
</Row>
</Pressable>
);
};
interface VcItemProps {
vcKey: string;
margin?: string;
selectable?: boolean;
selected?: boolean;
onPress?: (vcRef?: ActorRefFrom<typeof vcItemMachine>) => void;
}
function getLocalizedField(rawField: string | LocalizedField) {
if (typeof rawField === 'string') {
return rawField;
}
try {
const locales: LocalizedField[] = JSON.parse(JSON.stringify(rawField));
return locales[0].value;
} catch (e) {
return '';
}
}

View File

@@ -18,6 +18,7 @@ export const Button: React.FC<ButtonProps> = (props) => {
Theme.ButtonStyles.container,
props.disabled ? Theme.ButtonStyles.disabled : null,
props.margin ? Theme.spacing('margin', props.margin) : null,
props.styles,
];
const handleOnPress = (event: GestureResponderEvent) => {
@@ -65,4 +66,5 @@ interface ButtonProps {
raised?: boolean;
loading?: boolean;
icon?: RNEButtonProps['icon'];
styles?: StyleProp<ViewStyle>;
}

View File

@@ -168,7 +168,7 @@
83CBB9F71A601CBA00E9B192 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1130;
LastUpgradeCheck = 1340;
TargetAttributes = {
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = 9L83VVTX8B;
@@ -250,52 +250,9 @@
files = (
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-MOSIPResidentApp/Pods-MOSIPResidentApp-resources.sh",
"${PODS_CONFIGURATION_BUILD_DIR}/EXConstants/EXConstants.bundle",
"${PODS_CONFIGURATION_BUILD_DIR}/EXUpdates/EXUpdates.bundle",
"${PODS_ROOT}/NearbyMessages/Resources/resources/GNSSharedResources.bundle",
"${PODS_ROOT}/NearbyMessages/Resources/resources/ic_expand_more.xcassets",
"${PODS_ROOT}/NearbyMessages/Resources/resources/ic_nearby_48pt.xcassets",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/AntDesign.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Entypo.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Feather.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Brands.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Regular.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/FontAwesome5_Solid.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Fontisto.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Foundation.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Octicons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf",
"${PODS_ROOT}/../../node_modules/react-native-vector-icons/Fonts/Zocial.ttf",
"${PODS_CONFIGURATION_BUILD_DIR}/React-Core/AccessibilityResources.bundle",
);
name = "[CP] Copy Pods Resources";
outputPaths = (
"${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}/AntDesign.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Entypo.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/EvilIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Feather.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Brands.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Regular.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FontAwesome5_Solid.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Fontisto.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Foundation.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Ionicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialCommunityIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MaterialIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Octicons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/SimpleLineIcons.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Zocial.ttf",
"${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/AccessibilityResources.bundle",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
@@ -345,7 +302,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = MOSIPResidentApp/MOSIPResidentApp.entitlements;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 10.5;
DEVELOPMENT_TEAM = 9L83VVTX8B;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -378,7 +335,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = MOSIPResidentApp/MOSIPResidentApp.entitlements;
CURRENT_PROJECT_VERSION = 4;
CURRENT_PROJECT_VERSION = 10.5;
DEVELOPMENT_TEAM = 9L83VVTX8B;
INFOPLIST_FILE = MOSIPResidentApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
@@ -421,6 +378,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -479,6 +437,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -503,6 +462,7 @@
LIBRARY_SEARCH_PATHS = "\"$(inherited)\"";
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
VALIDATE_PRODUCT = YES;
};
name = Release;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1130"
LastUpgradeVersion = "1340"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -52,8 +52,8 @@
<string>Bluetooth is used to allow sharing VCs with another device</string>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location access is required for the scanning functionality</string>
<key>NSFaceIDUsageDescription</key>
<string>Resident app can be unlocked using Face ID</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>UILaunchStoryboardName</key>

View File

@@ -1,3 +1,5 @@
install! 'cocoapods', :disable_input_output_paths => true
require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking")
require File.join(File.dirname(`node --print "require.resolve('react-native/package.json')"`), "scripts/react_native_pods")
require File.join(File.dirname(`node --print "require.resolve('@react-native-community/cli-platform-ios/package.json')"`), "native_modules")

View File

@@ -39,7 +39,7 @@ PODS:
- ExpoModulesCore
- React-Core
- EXStructuredHeaders (2.1.1)
- EXUpdates (0.11.6):
- EXUpdates (0.11.7):
- EXManifests
- ExpoModulesCore
- EXStructuredHeaders
@@ -55,6 +55,26 @@ PODS:
- React-jsi (= 0.64.3)
- ReactCommon/turbomodule/core (= 0.64.3)
- glog (0.3.5)
- GoogleInterchangeUtilities (1.2.2):
- GoogleSymbolUtilities (~> 1.1)
- GoogleNetworkingUtilities (1.2.2):
- GoogleSymbolUtilities (~> 1.1)
- GoogleSymbolUtilities (1.1.2)
- GoogleUtilitiesLegacy (1.3.2):
- GoogleSymbolUtilities (~> 1.1)
- NearbyMessages (1.1.1):
- GoogleInterchangeUtilities (~> 1.2)
- GoogleNetworkingUtilities (~> 1.2)
- GoogleSymbolUtilities (~> 1.1)
- GoogleUtilitiesLegacy (~> 1.3)
- Permission-BluetoothPeripheral (3.6.0):
- RNPermissions
- Permission-Camera (3.6.0):
- RNPermissions
- Permission-LocationAccuracy (3.6.0):
- RNPermissions
- Permission-LocationWhenInUse (3.6.0):
- RNPermissions
- RCT-Folly (2020.01.13.00):
- boost-for-react-native
- DoubleConversion
@@ -65,8 +85,6 @@ PODS:
- DoubleConversion
- glog
- RCTRequired (0.64.3)
- RCTSystemSetting (1.7.6):
- React
- RCTTypeSafety (0.64.3):
- FBLazyVector (= 0.64.3)
- RCT-Folly (= 2020.01.13.00)
@@ -330,6 +348,8 @@ PODS:
- React-Core
- RNKeychain (8.0.0):
- React-Core
- RNPermissions (3.6.0):
- React-Core
- RNScreens (3.10.2):
- React-Core
- React-RCTImage
@@ -339,7 +359,8 @@ PODS:
- React
- RNVectorIcons (8.1.0):
- React-Core
- smartshare-react-native (0.2.2):
- smartshare-react-native (0.2.3-beta.2):
- NearbyMessages
- React-Core
- Yoga (1.14.0)
- ZXingObjC/Core (3.6.5)
@@ -372,9 +393,12 @@ DEPENDENCIES:
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- Permission-BluetoothPeripheral (from `../node_modules/react-native-permissions/ios/BluetoothPeripheral`)
- Permission-Camera (from `../node_modules/react-native-permissions/ios/Camera`)
- Permission-LocationAccuracy (from `../node_modules/react-native-permissions/ios/LocationAccuracy`)
- Permission-LocationWhenInUse (from `../node_modules/react-native-permissions/ios/LocationWhenInUse`)
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
- RCTRequired (from `../node_modules/react-native/Libraries/RCTRequired`)
- RCTSystemSetting (from `../node_modules/react-native-system-setting`)
- RCTTypeSafety (from `../node_modules/react-native/Libraries/TypeSafety`)
- React (from `../node_modules/react-native/`)
- React-callinvoker (from `../node_modules/react-native/ReactCommon/callinvoker`)
@@ -406,6 +430,7 @@ DEPENDENCIES:
- RNDeviceInfo (from `../node_modules/react-native-device-info`)
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
- RNKeychain (from `../node_modules/react-native-keychain`)
- RNPermissions (from `../node_modules/react-native-permissions`)
- RNScreens (from `../node_modules/react-native-screens`)
- RNSecureRandom (from `../node_modules/react-native-securerandom`)
- RNSVG (from `../node_modules/react-native-svg`)
@@ -416,6 +441,11 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- boost-for-react-native
- GoogleInterchangeUtilities
- GoogleNetworkingUtilities
- GoogleSymbolUtilities
- GoogleUtilitiesLegacy
- NearbyMessages
- ZXingObjC
EXTERNAL SOURCES:
@@ -465,12 +495,18 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/React/FBReactNativeSpec"
glog:
:podspec: "../node_modules/react-native/third-party-podspecs/glog.podspec"
Permission-BluetoothPeripheral:
:path: "../node_modules/react-native-permissions/ios/BluetoothPeripheral"
Permission-Camera:
:path: "../node_modules/react-native-permissions/ios/Camera"
Permission-LocationAccuracy:
:path: "../node_modules/react-native-permissions/ios/LocationAccuracy"
Permission-LocationWhenInUse:
:path: "../node_modules/react-native-permissions/ios/LocationWhenInUse"
RCT-Folly:
:podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
RCTRequired:
:path: "../node_modules/react-native/Libraries/RCTRequired"
RCTSystemSetting:
:path: "../node_modules/react-native-system-setting"
RCTTypeSafety:
:path: "../node_modules/react-native/Libraries/TypeSafety"
React:
@@ -529,6 +565,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-gesture-handler"
RNKeychain:
:path: "../node_modules/react-native-keychain"
RNPermissions:
:path: "../node_modules/react-native-permissions"
RNScreens:
:path: "../node_modules/react-native-screens"
RNSecureRandom:
@@ -562,14 +600,22 @@ SPEC CHECKSUMS:
ExpoModulesCore: 32c0ccb47f477d330ee93db72505380adf0de09a
EXSplashScreen: 21669e598804ee810547dbb6692c8deb5dd8dbf3
EXStructuredHeaders: 4993087b2010dbcc510f5d92555b36f523425e8d
EXUpdates: bd5fa64f02685ed3e96be86b5ca350cdc2cd8d02
EXUpdates: a83e036243b0f6ece53a8c1cb883b6751c88a5f8
EXUpdatesInterface: a9814f422d3cd6e7cfd260d13c27786148ece20e
FBLazyVector: c71c5917ec0ad2de41d5d06a5855f6d5eda06971
FBReactNativeSpec: b4cea6dceefc7cf1b25592b7aa7d9be51e7ef013
FBReactNativeSpec: 2492beb27d7c97438138799b65e5914def868da4
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
GoogleInterchangeUtilities: d5bc4d88d5b661ab72f9d70c58d02ca8c27ad1f7
GoogleNetworkingUtilities: 3edd3a8161347494f2da60ea0deddc8a472d94cb
GoogleSymbolUtilities: 631ee17048aa5e9ab133470d768ea997a5ef9b96
GoogleUtilitiesLegacy: 5501bedec1646bd284286eb5fc9453f7e23a12f4
NearbyMessages: bd9e88f2df7fbab78be58fed58580d5d5bd62cbf
Permission-BluetoothPeripheral: 2a5154a9dfdb1cfcf1d546650ced9671904a02af
Permission-Camera: 0a0fb4341f50ab242f496fb2f73380e0ec454fe7
Permission-LocationAccuracy: 13cbce13607e0738f1339447b4c5f51aa2c2b597
Permission-LocationWhenInUse: 51aa065819cd582517e98e89b564e2465a4a83c6
RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c
RCTRequired: d34bf57e17cb6e3b2681f4809b13843c021feb6c
RCTSystemSetting: 5107b7350d63b3f7b42a1277d07e4e5d9df879be
RCTTypeSafety: 8dab4933124ed39bb0c1d88d74d61b1eb950f28f
React: ef700aeb19afabff83a9cc5799ac955a9c6b5e0f
React-callinvoker: 5547633d44f3e114b17c03c660ccb5faefd9ed2d
@@ -599,14 +645,15 @@ SPEC CHECKSUMS:
RNDeviceInfo: 36286df381fcaf1933ff9d2d3c34ba2abeb2d8d8
RNGestureHandler: e1099204721a17a89c81fcd1cc2e92143dc040fb
RNKeychain: 4f63aada75ebafd26f4bc2c670199461eab85d94
RNPermissions: de7b7c3fe1680d974ac7a85e3e97aa539c0e68ea
RNScreens: d6da2b9e29cf523832c2542f47bf1287318b1868
RNSecureRandom: 0dcee021fdb3d50cd5cee5db0ebf583c42f5af0e
RNSVG: 551acb6562324b1d52a4e0758f7ca0ec234e278f
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
smartshare-react-native: 5c8c6ece55cf7643e4d1d5f8d9edd7235d9b28fc
smartshare-react-native: 133dca4c48dea0908649c680701f0948317378c5
Yoga: e6ecf3fa25af9d4c87e94ad7d5d292eedef49749
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
PODFILE CHECKSUM: a424a967159c32f02aadbfc51d78057f122a72df
PODFILE CHECKSUM: 24c315d126fc2b5fef6a2828f16ec02b341154fe
COCOAPODS: 1.11.3

View File

@@ -5,6 +5,11 @@
"deviceRefNumber": "Device reference number",
"name": "Name"
},
"OIDcAuth": {
"title": "OIDC Authentication",
"text": "To be replaced with the OIDC provider UI",
"verify": "Verify"
},
"PasscodeVerify": {
"passcodeMismatchError": "Passcode did not match."
},
@@ -40,7 +45,7 @@
}
},
"BiometricScreen": {
"unlock": "Unlock with fingerprint"
"unlock": "Unlock with biometrics"
},
"HistoryTab": {
"noHistory": "No history available yet",
@@ -124,13 +129,21 @@
"noReceivedVcsText": "Tap on Request below to receive {{vcLabel}}"
},
"ViewVcModal": {
"cancel": "Cancel",
"lock": "Lock",
"unlock": "Unlock",
"rename": "Rename",
"delete": "Delete",
"revoke": "Revoke",
"revoking": "Your wallet contains a credential with VID {{vid}}. Revoking this will automatically remove the same from the wallet. Are you sure you want to proceed?",
"requestingOtp": "Requesting OTP...",
"editTag": "Edit Tag"
"editTag": "Rename",
"redirecting": "Redirecting...",
"success": {
"unlocked": "{{vcLabel}} successfully unlocked",
"locked": "{{vcLabel}} successfully locked",
"revoked": "VID {{vid}} has been revoked. Any credential containing the same will be removed automatically from the wallet"
}
},
"MainLayout": {
"home": "Home",
@@ -154,7 +167,13 @@
"bioUnlock": "Biometric unlock",
"authFactorUnlock": "Unlock auth factor",
"credits": "Credits and legal notices",
"logout": "Log-out"
"logout": "Log-out",
"revokeLabel": "Revoke VID",
"revokeHeader": "REVOKE VID",
"revokingVids": "You are about to revoke ({{count}}) VIDs.",
"revokingVidsAfter": "This means you will no longer be able to use or view any of the IDs linked to those VID(s). \nAre you sure you want to proceed?",
"empty": "Empty",
"revokeSuccessful": "VID successfully revoked"
},
"ReceiveVcScreen": {
"header": "{{vcLabel}} details",
@@ -180,8 +199,14 @@
"message": "The connection was interrupted. Please try again."
},
"waitingConnection": "Waiting for connection...",
"exchangingDeviceInfo": "Exchanging device info...",
"connected": "Connected to device. Waiting for {{vcLabel}}..."
"exchangingDeviceInfo": {
"message": "Exchanging device info...",
"timeoutHint": "It's taking too long to exchange device info..."
},
"connected": {
"message": "Connected to device. Waiting for {{vcLabel}}...",
"timeoutHint": "No VC data received yet. Is sending device still connected?"
}
},
"online": "Online",
"offline": "Offline",
@@ -192,10 +217,6 @@
"noShareableVcs": "No shareable {{vcLabel}} are available.",
"sharingVc": "Sharing {{vcLabel}}",
"errors": {
"flightMode": {
"message": "Flight mode must be disabled for the scanning functionality",
"button": "Disable flight mode"
},
"locationDisabled": {
"message": "Location services must be enabled for the scanning functionality",
"button": "Enable location services"
@@ -207,30 +228,42 @@
},
"status": {
"connecting": "Connecting...",
"connectingTimeout": "It's taking a while to establish connection. Is the other device open for connections?",
"exchangingDeviceInfo": "Exchanging device info...",
"exchangingDeviceInfoTimeout": "It's taking a while to exchange device info. You may have to reconnect.",
"invalid": "Invalid QR Code"
}
},
"SelectVcOverlay": {
"header": "Share {{vcLabel}}",
"chooseVc": "Choose the {{vcLabel}} you'd like to share with",
"cancel": "Cancel",
"share": "Share"
"share": "Share",
"verifyAndShare": "Verify Identity & Share"
},
"SendVcModal": {
"SendVcScreen": {
"reasonForSharing": "Reason for sharing (optional)",
"acceptRequest": "Accept request and choose {{vcLabel}}",
"reject": "Reject",
"statusSharing": {
"title": "Sharing..."
"status": {
"sharing": {
"title": "Sharing...",
"timeoutHint": "It's taking a while to share VC. There could be a problem with the connection."
},
"accepted": {
"title": "Success!",
"message": "Your {{vcLabel}} has been successfully shared with {{receiver}}"
},
"rejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
},
"verifyingIdentity": "Verifying identity..."
},
"statusAccepted": {
"title": "Success!",
"message": "Your {{vcLabel}} has been successfully shared with {{receiver}}"
},
"statusRejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
"errors": {
"invalidIdentity": {
"title": "Unable to verify identity",
"message": "An error occured and we couldn't scan your portrait. Try again, make sure your face is visible, devoid of any accessories."
}
}
},
"WelcomeScreen": {
@@ -241,6 +274,7 @@
"common": {
"cancel": "Cancel",
"save": "Save",
"editLabel": "Edit {{label}}"
"editLabel": "Edit {{label}}",
"tryAgain": "Try again"
}
}

View File

@@ -5,6 +5,11 @@
"deviceRefNumber": "Reference number ng device",
"name": "Pangalan"
},
"OIDcAuth": {
"title": "OIDC Authentication",
"text": "Papalitan ng OIDC service provider UI",
"verify": "I-verify"
},
"PasscodeVerify": {
"passcodeMismatchError": "Hindi tumugma ang passcode."
},
@@ -37,7 +42,7 @@
}
},
"BiometricScreen": {
"unlock": "I-unlock gamit ang fingerprint"
"unlock": "I-unlock gamit ang biometrics"
},
"HistoryTab": {
"noHistory": "Wala pang kasaysayan",
@@ -99,13 +104,21 @@
"noReceivedVcsText": "Pindutin ang Humiling sa ibaba para makatanggap ng {{vcLabel}}"
},
"ViewVcModal": {
"cancel": "Kanselahin",
"lock": "Isara ang paggamit",
"unlock": "Buksan ang paggamit",
"rename": "Palitan ang pangalan",
"delete": "Tanggalin",
"revoke": "Bawiin",
"revoking": "Ang iyong wallet ay naglalaman ng kredensyal na may VID {{vid}}. Ang pagkansela nito ay awtomatikong mag-aalis ng pareho sa wallet. Sigurado ka bang gusto mong magpatuloy?",
"requestingOtp": "Humihiling ng OTP...",
"editTag": "Palitan ang Tag"
"editTag": "Palitan ang pangalan",
"redirecting": "Redirecting...",
"success": {
"unlocked": "Ang {{vcLabel}} ay matagumpay na na-unlock",
"locked": "Ang {{vcLabel}} ay matagumpay na na-lock",
"revoked": "Ang VID {{vid}} ay nakansela. Ang lahat ng mga detalye na naglalaman ng pareho ay awtomatikong aalisin wallet."
}
},
"MainLayout": {
"home": "Home",
@@ -129,17 +142,24 @@
"bioUnlock": "Pagbukas gamit Biometric",
"authFactorUnlock": "Pagbukas ng auth factor",
"credits": "Mga kredito at legal na abiso",
"logout": "Mag log-out"
"logout": "Mag log-out",
"revokeLabel": "Kanselahin ang VID",
"revokeHeader": "KANSELAHIN ANG VID",
"revokingVids": "Kakanselahin mo na ang ({{count}}) na mga VID.",
"revokingVidsAfter": "Nangangahulugan ito na hindi mo na maa-access o matitingnan ang anumang mga ID na naka-link sa mga VID na ito. Sigurado ka bang gusto mong magpatuloy?",
"empty": "Walang laman",
"revokeSuccessful": "Matagumpay na nakansela ang VID"
},
"ReceiveVcModal": {
"header": "Detalye ng {{vcLabel}}",
"acceptRequest": "Tanggapin ang kahilingan at kunin ang {{vcLabel}}",
"ReceiveVcScreen": {
"header": "Mga detalye ng {{vcLabel}}",
"acceptRequest": "Tanggapin ang kahilingan at tumanggap ng {{vcLabel}}",
"reject": "Tanggihan"
},
"RequestScreen": {
"bluetoothDenied": "Mangyaring paganahin ang Bluetooth upang makahiling ng {{vcLabel}}",
"showQrCode": "Ipakita ang QR code na ito para makahiling ng {{vcLabel}}",
"incomingVc": "Padating na {{vcLabel}}",
"request": "Hilingin",
"status": {
"accepted": {
"title": "Tagumpay!",
@@ -154,19 +174,24 @@
"message": "Naputol ang koneksyon. Pakiulit."
},
"waitingConnection": "Naghihintay ng koneksyon...",
"exchangingDeviceInfo": "Nagpapalitan ng impormasyon ng device...",
"connected": "Nakakonekta sa device. Naghihintay ng {{vcLabel}}..."
}
"exchangingDeviceInfo": {
"message": "Pagpapalitan ng impormasyon ng device...",
"timeoutHint": "Masyadong matagal ang pagpapalitan ng impormasyon ng device..."
},
"connected": {
"message": "Nakakonektang device. Naghihintay para sa {{vcLabel}}...",
"timeoutHint": "Wala pang natanggap na VC. Nakakonekta pa rin ba ang pagpapadala ng device?"
}
},
"online": "Online",
"offline": "Offline",
"gotoSettings": "Pumunta sa setting"
},
"ScanScreen": {
"header": "I-scan ang QR Code",
"noShareableVcs": "Walang magagamit na maibabahaging {{vcLabel}}.",
"sharingVc": "Pagbabahagi ng {{vcLabel}}",
"errors": {
"flightMode": {
"message": "Dapat na hindi nakabukas ang flight mode ng iyong mobile para maaaring makapag-scan",
"button": "Isara ang flight mode"
},
"locationDisabled": {
"message": "Dapat na nakabukas ang Location services ng iyong mobile para maaaring makapag-scan",
"button": "Buksan ang location services"
@@ -178,30 +203,42 @@
},
"status": {
"connecting": "Kumokonekta...",
"connectingTimeout": "Medyo nagtatagal bago magtatag ng koneksyon. Bukas ba ang ibang device para sa mga koneksyon?",
"exchangingDeviceInfo": "Nagpapalitan ng impormasyon ng device...",
"exchangingDeviceInfoTimeout": "Medyo nagtatagal ang paglabas ng impormasyon ng device. Bukas ba ang ibang device para sa mga koneksyon?",
"invalid": "Di-wasto ang QR Code"
}
},
"SelectVcOverlay": {
"header": "Ibahagi ang {{vcLabel}}",
"chooseVc": "Piliin ang {{vcLabel}} na gusto mong ibahagi",
"cancel": "Kanselahin",
"share": "Ibahagi"
"share": "Ibahagi",
"verifyAndShare": "I-verify ang Pagkakakilanlan at Ibahagi"
},
"SendVcModal": {
"reasonForSharing": "Dahilan ng pagbabahagi (hindi ubligado)",
"SendVcScreen": {
"reasonForSharing": "Dahilan ng pagbabahagi (opsyonal)",
"acceptRequest": "Tanggapin ang kahilingan at piliin ang {{vcLabel}}",
"reject": "Tanggihan",
"statusSharing": {
"title": "Pagbabahagi..."
"status": {
"sharing": {
"title": "Pagbabahagi",
"timeoutHint": "Medyo natatagal ang pagbabahagi ng VC. Maaaring may problema sa koneksyon."
},
"accepted": {
"title": "Tagumpay!",
"message": "Ang iyong {{vcLabel}} ay matagumpay na naibahagi kay {{receiver}}"
},
"rejected": {
"title": "Pansinin",
"message": "Ang iyong {{vcLabel}} ay tinanggihan ng {{receiver}}"
},
"verifyingIdentity": "Bine-verify ang pagkakakilanlan..."
},
"statusAccepted": {
"title": "Tagumpay!",
"message": "Ang iyong {{vcLabel}} ay matagumpay na naibahagi sa {{receiver}}"
},
"statusRejected": {
"title": "Paunawa",
"message": "Ang iyong {{vcLabel}} ay tinanggihan ni {{receiver}}"
"errors": {
"invalidIdentity": {
"title": "Hindi ma-verify ang pagkakakilanlan",
"message": "May naganap na error at hindi namin ma-scan ang iyong portrait. Subukang muli, tiyaking nakikita ang iyong mukha, walang anumang mga accessory."
}
}
},
"WelcomeScreen": {
@@ -212,6 +249,7 @@
"common": {
"cancel": "Kanselahin",
"save": "I-save",
"editLabel": "Palitan ang {{label}}"
"editLabel": "Palitan ang {{label}}",
"tryAgain": "Subukan muli"
}
}

View File

@@ -12,7 +12,7 @@ const model = createModel(
{
events: {
STORE_RESPONSE: (response: unknown) => ({ response }),
LOG_ACTIVITY: (log: ActivityLog) => ({ log }),
LOG_ACTIVITY: (log: ActivityLog | ActivityLog[]) => ({ log }),
REFRESH: () => ({}),
},
}
@@ -93,7 +93,9 @@ export const activityLogMachine =
prependActivity: model.assign({
activities: (context, event) =>
[event.response, ...context.activities] as ActivityLog[],
(Array.isArray(event.response)
? [...event.response, ...context.activities]
: [event.response, ...context.activities]) as ActivityLog[],
}),
},
}
@@ -118,7 +120,8 @@ export type ActivityLogAction =
| 'shared'
| 'received'
| 'deleted'
| 'downloaded';
| 'downloaded'
| 'revoked';
type State = StateFrom<typeof activityLogMachine>;

View File

@@ -2,12 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
setActivities: 'STORE_RESPONSE';
prependActivity: 'STORE_RESPONSE';
loadActivities: 'REFRESH';
storeActivity: 'LOG_ACTIVITY';
};
'internalEvents': {
'xstate.init': { type: 'xstate.init' };
};
@@ -18,6 +12,12 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
loadActivities: 'REFRESH' | 'xstate.init';
prependActivity: 'STORE_RESPONSE';
setActivities: 'STORE_RESPONSE';
storeActivity: 'LOG_ACTIVITY';
};
'eventsCausingServices': {};
'eventsCausingGuards': {};
'eventsCausingDelays': {};

View File

@@ -14,6 +14,8 @@ import { createVcMachine, vcMachine } from './vc';
import { createActivityLogMachine, activityLogMachine } from './activityLog';
import { createRequestMachine, requestMachine } from './request';
import { createScanMachine, scanMachine } from './scan';
import { createRevokeMachine, revokeVidsMachine } from './revoke';
import { pure, respond } from 'xstate/lib/actions';
import { AppServices } from '../shared/GlobalContext';
import { request } from '../shared/request';
@@ -172,15 +174,19 @@ export const appMachine = model.createMachine(
const serviceRefs = {
...context.serviceRefs,
};
serviceRefs.auth = spawn(
createAuthMachine(serviceRefs),
authMachine.id
);
serviceRefs.vc = spawn(createVcMachine(serviceRefs), vcMachine.id);
serviceRefs.settings = spawn(
createSettingsMachine(serviceRefs),
settingsMachine.id
);
serviceRefs.activityLog = spawn(
createActivityLogMachine(serviceRefs),
activityLogMachine.id
@@ -190,10 +196,17 @@ export const appMachine = model.createMachine(
createScanMachine(serviceRefs),
scanMachine.id
);
serviceRefs.request = spawn(
createRequestMachine(serviceRefs),
requestMachine.id
);
serviceRefs.revoke = spawn(
createRevokeMachine(serviceRefs),
revokeVidsMachine.id
);
return serviceRefs;
},
}),
@@ -206,6 +219,7 @@ export const appMachine = model.createMachine(
context.serviceRefs.activityLog.subscribe(logState);
context.serviceRefs.scan.subscribe(logState);
context.serviceRefs.request.subscribe(logState);
context.serviceRefs.revoke.subscribe(logState);
}
},

View File

@@ -2,13 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
setContext: 'STORE_RESPONSE';
setPasscode: 'SETUP_PASSCODE';
storeContext: 'SETUP_PASSCODE' | 'SETUP_BIOMETRICS' | 'STORE_RESPONSE';
setBiometrics: 'SETUP_BIOMETRICS';
requestStoredContext: 'xstate.init';
};
'internalEvents': {
'': { type: '' };
'xstate.init': { type: 'xstate.init' };
@@ -20,19 +13,26 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
requestStoredContext: 'xstate.init';
setBiometrics: 'SETUP_BIOMETRICS';
setContext: 'STORE_RESPONSE';
setPasscode: 'SETUP_PASSCODE';
storeContext: 'SETUP_BIOMETRICS' | 'SETUP_PASSCODE' | 'STORE_RESPONSE';
};
'eventsCausingServices': {};
'eventsCausingGuards': {
hasBiometricSet: '';
hasData: 'STORE_RESPONSE';
hasPasscodeSet: '';
hasBiometricSet: '';
};
'eventsCausingDelays': {};
'matchesStates':
| 'authorized'
| 'checkingAuth'
| 'init'
| 'savingDefaults'
| 'checkingAuth'
| 'settingUp'
| 'unauthorized'
| 'authorized';
| 'unauthorized';
'tags': never;
}

View File

@@ -2,22 +2,11 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
selectInput: 'FOCUS_INPUT';
updateInput: 'UPDATE_INPUT';
focusSelected:
| 'xstate.after(INITIAL_FOCUS_DELAY)#pinInput.idle'
| 'UPDATE_INPUT'
| 'KEY_PRESS';
selectNextInput: 'UPDATE_INPUT';
selectPrevInput: 'KEY_PRESS';
clearInput: 'KEY_PRESS';
};
'internalEvents': {
'': { type: '' };
'xstate.after(INITIAL_FOCUS_DELAY)#pinInput.idle': {
type: 'xstate.after(INITIAL_FOCUS_DELAY)#pinInput.idle';
};
'': { type: '' };
'xstate.init': { type: 'xstate.init' };
};
'invokeSrcNameMap': {};
@@ -27,14 +16,25 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
clearInput: 'KEY_PRESS';
focusSelected:
| 'KEY_PRESS'
| 'UPDATE_INPUT'
| 'xstate.after(INITIAL_FOCUS_DELAY)#pinInput.idle';
selectInput: 'FOCUS_INPUT';
selectNextInput: 'UPDATE_INPUT';
selectPrevInput: 'KEY_PRESS';
updateInput: 'UPDATE_INPUT';
};
'eventsCausingServices': {};
'eventsCausingGuards': {
isBlank: 'UPDATE_INPUT';
hasNextInput: 'UPDATE_INPUT';
canGoBack: 'KEY_PRESS';
hasNextInput: 'UPDATE_INPUT';
isBlank: 'UPDATE_INPUT';
};
'eventsCausingDelays': {
INITIAL_FOCUS_DELAY: 'xstate.init';
INITIAL_FOCUS_DELAY: '' | 'xstate.init';
};
'matchesStates': 'idle' | 'selectingNext' | 'selectingPrev';
'tags': never;

View File

@@ -31,6 +31,9 @@ import {
type SharingProtocol = 'OFFLINE' | 'ONLINE';
const waitingForConnectionId = '#waitingForConnection';
const checkingBluetoothServiceId = '#checkingBluetoothService';
const model = createModel(
{
serviceRefs: {} as AppServices,
@@ -80,7 +83,7 @@ export const requestMachine = model.createMachine(
id: 'request',
initial: 'inactive',
invoke: {
src: 'checkConnection',
src: 'monitorConnection',
},
on: {
SCREEN_BLUR: 'inactive',
@@ -95,6 +98,7 @@ export const requestMachine = model.createMachine(
entry: ['removeLoggers'],
},
checkingBluetoothService: {
id: 'checkingBluetoothService',
initial: 'checking',
states: {
checking: {
@@ -165,6 +169,23 @@ export const requestMachine = model.createMachine(
actions: ['setSenderInfo'],
},
},
initial: 'inProgress',
states: {
inProgress: {},
timeout: {
on: {
CANCEL: {
actions: 'disconnect',
target: checkingBluetoothServiceId,
},
},
},
},
after: {
CONNECTION_TIMEOUT: {
target: '.timeout',
},
},
},
waitingForVc: {
invoke: {
@@ -177,6 +198,23 @@ export const requestMachine = model.createMachine(
actions: ['setIncomingVc'],
},
},
initial: 'inProgress',
states: {
inProgress: {},
timeout: {
on: {
CANCEL: {
actions: 'disconnect',
target: checkingBluetoothServiceId,
},
},
},
},
after: {
CONNECTION_TIMEOUT: {
target: '.timeout',
},
},
},
reviewing: {
on: {
@@ -251,7 +289,7 @@ export const requestMachine = model.createMachine(
},
},
on: {
DISMISS: '#waitingForConnection',
DISMISS: waitingForConnectionId,
},
},
navigatingToHome: {},
@@ -259,6 +297,7 @@ export const requestMachine = model.createMachine(
exit: ['disconnect'],
},
disconnected: {
id: 'disconnected',
entry: ['disconnect'],
on: {
DISMISS: 'waitingForConnection',
@@ -269,7 +308,9 @@ export const requestMachine = model.createMachine(
{
actions: {
openSettings: () => {
Linking.openURL('App-Prefs:Bluetooth');
Platform.OS === 'android'
? BluetoothStateManager.openSettings().catch()
: Linking.openURL('App-Prefs:Bluetooth');
},
switchProtocol: assign({
@@ -467,14 +508,16 @@ export const requestMachine = model.createMachine(
}
},
checkConnection: () => (callback) => {
const subscription = IdpassSmartshare.handleNearbyEvents((event) => {
if (event.type === 'onDisconnected') {
callback({ type: 'DISCONNECT' });
}
});
monitorConnection: (context) => (callback) => {
if (context.sharingProtocol === 'OFFLINE') {
const subscription = IdpassSmartshare.handleNearbyEvents((event) => {
if (event.type === 'onDisconnected') {
callback({ type: 'DISCONNECT' });
}
});
return () => subscription.remove();
return () => subscription.remove();
}
},
exchangeDeviceInfo: (context) => (callback) => {
@@ -570,6 +613,9 @@ export const requestMachine = model.createMachine(
delays: {
CLEAR_DELAY: 250,
CONNECTION_TIMEOUT: () => {
return (Platform.OS === 'ios' ? 10 : 5) * 1000;
},
},
}
);
@@ -623,12 +669,24 @@ export function selectIsBluetoothDenied(state: State) {
return state.matches('bluetoothDenied');
}
export function selectIsCheckingBluetoothService(state: State) {
return state.matches('checkingBluetoothService');
}
export function selectIsExchangingDeviceInfo(state: State) {
return state.matches('exchangingDeviceInfo');
return state.matches('exchangingDeviceInfo.inProgress');
}
export function selectIsExchangingDeviceInfoTimeout(state: State) {
return state.matches('exchangingDeviceInfo.timeout');
}
export function selectIsWaitingForVc(state: State) {
return state.matches('waitingForVc');
return state.matches('waitingForVc.inProgress');
}
export function selectIsWaitingForVcTimeout(state: State) {
return state.matches('waitingForVc.timeout');
}
export function selectIsDone(state: State) {

View File

@@ -13,8 +13,8 @@ export interface Typegen0 {
'invokeSrcNameMap': {
advertiseDevice: 'done.invoke.waitingForConnection:invocation[0]';
checkBluetoothService: 'done.invoke.request.checkingBluetoothService.checking:invocation[0]';
checkConnection: 'done.invoke.request:invocation[0]';
exchangeDeviceInfo: 'done.invoke.request.exchangingDeviceInfo:invocation[0]';
monitorConnection: 'done.invoke.request:invocation[0]';
receiveVc: 'done.invoke.request.waitingForVc:invocation[0]';
requestBluetooth: 'done.invoke.request.checkingBluetoothService.requesting:invocation[0]';
sendVcResponse:
@@ -30,6 +30,7 @@ export interface Typegen0 {
'eventsCausingActions': {
disconnect:
| ''
| 'CANCEL'
| 'DISCONNECT'
| 'DISMISS'
| 'SCREEN_BLUR'
@@ -63,13 +64,13 @@ export interface Typegen0 {
};
'eventsCausingServices': {
advertiseDevice: 'DISMISS' | 'xstate.after(CLEAR_DELAY)#clearingConnection';
checkBluetoothService: 'SCREEN_FOCUS' | 'SWITCH_PROTOCOL';
checkConnection:
checkBluetoothService: 'CANCEL' | 'SCREEN_FOCUS' | 'SWITCH_PROTOCOL';
exchangeDeviceInfo: 'RECEIVE_DEVICE_INFO';
monitorConnection:
| 'SCREEN_BLUR'
| 'SCREEN_FOCUS'
| 'SWITCH_PROTOCOL'
| 'xstate.init';
exchangeDeviceInfo: 'RECEIVE_DEVICE_INFO';
receiveVc: 'EXCHANGE_DONE';
requestBluetooth: 'BLUETOOTH_DISABLED';
sendVcResponse: 'CANCEL' | 'REJECT' | 'STORE_RESPONSE';
@@ -79,6 +80,7 @@ export interface Typegen0 {
};
'eventsCausingDelays': {
CLEAR_DELAY: '';
CONNECTION_TIMEOUT: 'EXCHANGE_DONE' | 'RECEIVE_DEVICE_INFO';
};
'matchesStates':
| 'bluetoothDenied'
@@ -89,6 +91,8 @@ export interface Typegen0 {
| 'clearingConnection'
| 'disconnected'
| 'exchangingDeviceInfo'
| 'exchangingDeviceInfo.inProgress'
| 'exchangingDeviceInfo.timeout'
| 'inactive'
| 'preparingToExchangeInfo'
| 'reviewing'
@@ -104,8 +108,11 @@ export interface Typegen0 {
| 'reviewing.rejected'
| 'waitingForConnection'
| 'waitingForVc'
| 'waitingForVc.inProgress'
| 'waitingForVc.timeout'
| {
checkingBluetoothService?: 'checking' | 'enabled' | 'requesting';
exchangingDeviceInfo?: 'inProgress' | 'timeout';
reviewing?:
| 'accepted'
| 'accepting'
@@ -120,6 +127,7 @@ export interface Typegen0 {
| 'requestingReceivedVcs'
| 'storingVc';
};
waitingForVc?: 'inProgress' | 'timeout';
};
'tags': never;
}

298
machines/revoke.ts Normal file
View File

@@ -0,0 +1,298 @@
import { TextInput } from 'react-native';
import { assign, ErrorPlatformEvent, StateFrom, send, EventFrom } from 'xstate';
import { log } from 'xstate/lib/actions';
import i18n from '../i18n';
import { AppServices } from '../shared/GlobalContext';
import { ActivityLogEvents } from './activityLog';
import { StoreEvents } from './store';
import { createModel } from 'xstate/lib/model';
import { request } from '../shared/request';
import { VcIdType } from '../types/vc';
import { MY_VCS_STORE_KEY } from '../shared/constants';
const model = createModel(
{
serviceRefs: {} as AppServices,
idType: 'VID' as VcIdType,
idError: '',
otp: '',
otpError: '',
transactionId: '',
requestId: '',
VIDs: [] as string[],
},
{
events: {
INPUT_OTP: (otp: string) => ({ otp }),
VALIDATE_INPUT: () => ({}),
READY: (idInputRef: TextInput) => ({ idInputRef }),
DISMISS: () => ({}),
SELECT_ID_TYPE: (idType: VcIdType) => ({ idType }),
REVOKE_VCS: (vcKeys: string[]) => ({ vcKeys }),
STORE_RESPONSE: (response: string[]) => ({ response }),
},
}
);
export const revokeVidsMachine =
/** @xstate-layout N4IgpgJg5mDOIC5QCUwDcD2BrMA1AlhLAHSEA2YAxMgKK4DyA0jQPq4DCAyoqAA4ax8AF3wYAdjxAAPRACYAHAFZiARhUB2WQDZFABgCcO-boDMWgDQgAnohWyALLOK6X9k-J3zZirfYC+fpaomDgERKRiaACGZISUAJIAcgAKAKoAKiz06cmS-IIi4pIyCOq6TuomjvYqWlpe3iaWNgiKik4u5fK69vr2uipmAUHo2HiEJPiRMXEAIvGcALIL3Egg+cKiEmslZRVVDrX1so3Nclr6zi61pvLyKoY9wyDBY2EkUQDGn2C8ImJQXDxWaTCAUah0JisDirPgCTZFHaIEyydTEe6yEyaQzqXrtM4IeQmXRXcrqDz9dQPEzPV6hCbEL4-P5TQHAkgAJzAAEcAK5wf5QehCXiUCDiMAREKSunjcJM36CoEg4hcvkC1nC3gIKaYT5RQpiADaugAunl4Ybioh8dZbHtiHoevpFESlIp9DTAi9RvT5d9FazlZyefzYIKtZQwByORgOcReGQDQAzOMAW2IsvejIDLIBwdVoY1AK1OsiGH1hpN5rWGytSIQJix6LsWNkOLxsgJKLRnVMlXJdxq-m9WYZCrzbJBlHmSxWFoKW2tjdRLcx2K0uJdXbtCGMpN03Qe-X03VktN9co+uYjIviYl4vKECRSGSyOQXCO2oBKKkPaNdRQqXsRRBi0EwXQJep5EdFxURdTczB0C9pWzCdb14e9H2fWdlk4WF1ktJcGwUZQ1E0HQDCMUwLF3NQ+lJVFN3kXFvBQt4GTVMNBVlMUJSlMZM0vbMuOLKBZTLPUDS2atP3rH9bH-R0lGA0CzAgxQCRqS4nQ0F0-10SoRxGVDOKLcNWV46NY3jRMU3TITTPCUSLIBCTdQraTxFk2siMRBTSh3FpBhdWCeiAqkiSA4yfSckMQiDT4ZwWPCCLrYiAq8AlBgeYg6jKLF1DaB5ygCb0xAwCA4EkMdwnIMA5Iy6RbEMPKNGJSo2nuD17Gyh5lDqC59HUTwVBqL0TI4urpliCBiAwEVGv85rWipZx5GGxRiV8GjNN3BQALg8bMS0citHYv1JhmwhiAAIy+HAxAgJbvxWto0T6doTlkFwtBOFRuzaMK1G6TdqWGi6rylGZnt8xdlpKLaTGIT7vp+3Q-tkAH9pUZRdNOux9KxGLauvZklXZUgwQauGv2XH7LhMOwai+zcfvkXrd3sHQwsPal7C8WpFEhtCbyDSmXIwl7l00btud5twLj-Op5BF8cxfzdlpYbAXspYhW9iG3w1f9cnNTvB8n21gKBbROpHEAhn20UTngp+lRiCx7nN1uAxMRNkN1Vc8TL2tlaiTRQZtBMCKBe+gkXZJJ1tB6ECBf0FQA8LBL80+MPf3bGC7lRDHT1TptuzuMLDhRMwzoD-O5Fd2wYOuVF-raXx7HUMq-CAA */
model.createMachine(
{
tsTypes: {} as import('./revoke.typegen').Typegen0,
schema: {
context: model.initialContext,
events: {} as EventFrom<typeof model>,
},
id: 'RevokeVids',
initial: 'acceptingVIDs',
states: {
idle: {
on: {
REVOKE_VCS: {
actions: ['setTransactionId', 'clearOtp'],
target: 'acceptingOtpInput',
},
},
},
invalid: {
states: {
otp: {},
backend: {},
},
on: {
INPUT_OTP: {
actions: 'setOtp',
target: 'requestingRevoke',
},
DISMISS: {
target: 'idle',
},
},
},
acceptingVIDs: {
entry: ['setTransactionId', 'clearOtp'],
initial: 'idle',
states: {
idle: {
on: {
REVOKE_VCS: {
actions: 'setVIDs',
target: '#RevokeVids.acceptingOtpInput',
},
},
},
requestingOtp: {
invoke: {
src: 'requestOtp',
onDone: [
{
actions: log('accepting OTP'),
target: '#RevokeVids.acceptingOtpInput',
},
],
onError: [
{
actions: [log('error OTP'), 'setIdBackendError'],
target: '#RevokeVids.invalid.backend',
},
],
},
},
},
on: {
DISMISS: {
target: 'idle',
},
},
},
acceptingOtpInput: {
entry: 'clearOtp',
on: {
INPUT_OTP: {
actions: 'setOtp',
target: 'requestingRevoke',
},
DISMISS: {
target: 'idle',
},
},
},
requestingRevoke: {
invoke: {
src: 'requestRevoke',
onDone: {
target: 'revokingVc',
},
onError: {
actions: [log('error on Revoking'), 'setOtpError'],
target: 'acceptingOtpInput',
},
},
},
revokingVc: {
entry: ['revokeVID'],
on: {
STORE_RESPONSE: {
target: 'loggingRevoke',
},
},
},
loggingRevoke: {
entry: [log('loggingRevoke'), 'logRevoked'],
on: {
DISMISS: {
target: 'acceptingVIDs',
},
},
},
},
},
{
actions: {
setOtp: model.assign({
otp: (_context, event) => event.otp,
}),
setTransactionId: assign({
transactionId: () => String(new Date().valueOf()).substring(3, 13),
}),
setVIDs: model.assign({
VIDs: (_context, event) => event.vcKeys,
}),
setIdBackendError: assign({
idError: (context, event) => {
const message = (event as ErrorPlatformEvent).data.message;
const ID_ERRORS_MAP = {
'UIN invalid': 'invalidUin',
'VID invalid': 'invalidVid',
'UIN not available in database': 'missingUin',
'VID not available in database': 'missingVid',
'Invalid Input Parameter - individualId':
context.idType === 'UIN' ? 'invalidUin' : 'invalidVid',
};
return ID_ERRORS_MAP[message]
? i18n.t(`errors.backend.${ID_ERRORS_MAP[message]}`, {
ns: 'RevokeVids',
})
: message;
},
}),
setOtpError: assign({
otpError: (_context, event) => {
const message = (event as ErrorPlatformEvent).data.message;
const OTP_ERRORS_MAP = {
'OTP is invalid': 'invalidOtp',
};
return OTP_ERRORS_MAP[message]
? i18n.t(`errors.backend.${OTP_ERRORS_MAP[message]}`, {
ns: 'RevokeVids',
})
: message;
},
}),
clearOtp: assign({ otp: '' }),
logRevoked: send(
(context) =>
ActivityLogEvents.LOG_ACTIVITY(
context.VIDs.map((vc) => ({
_vcKey: vc,
action: 'revoked',
timestamp: Date.now(),
deviceName: '',
vcLabel: vc.split(':')[2],
}))
),
{
to: (context) => context.serviceRefs.activityLog,
}
),
revokeVID: send(
(context) => {
return StoreEvents.REMOVE_ITEMS(MY_VCS_STORE_KEY, context.VIDs);
},
{
to: (context) => context.serviceRefs.store,
}
),
},
services: {
requestOtp: async (context) => {
const transactionId = String(new Date().valueOf()).substring(3, 13);
return request('POST', '/req/otp', {
individualId: context.VIDs[0].split(':')[2],
individualIdType: 'VID',
otpChannel: ['EMAIL', 'PHONE'],
transactionID: transactionId,
});
},
requestRevoke: async (context) => {
try {
return await Promise.all(
context.VIDs.map((vid: string) => {
const vidID = vid.split(':')[2];
const transactionId = String(new Date().valueOf()).substring(
3,
13
);
return request('PATCH', `/vid/${vidID}`, {
transactionID: transactionId,
vidStatus: 'REVOKED',
individualId: vidID,
individualIdType: 'VID',
otp: context.otp,
});
})
);
} catch (error) {
console.error(error);
}
},
},
guards: {},
}
);
export function createRevokeMachine(serviceRefs: AppServices) {
return revokeVidsMachine.withContext({
...revokeVidsMachine.context,
serviceRefs,
});
}
type State = StateFrom<typeof revokeVidsMachine>;
export const RevokeVidsEvents = model.events;
export function selectIdType(state: State) {
return state.context.idType;
}
export function selectIdError(state: State) {
return state.context.idError;
}
export function selectOtpError(state: State) {
return state.context.otpError;
}
export function selectIsRevokingVc(state: State) {
return state.matches('revokingVc');
}
export function selectIsLoggingRevoke(state: State) {
return state.matches('loggingRevoke');
}
export function selectIsAcceptingOtpInput(state: State) {
return state.matches('acceptingOtpInput');
}

View File

@@ -0,0 +1,71 @@
// This file was automatically generated. Edits will be overwritten
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]': {
type: 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.RevokeVids.requestingRevoke:invocation[0]': {
type: 'done.invoke.RevokeVids.requestingRevoke:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]': {
type: 'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]';
data: unknown;
};
'error.platform.RevokeVids.requestingRevoke:invocation[0]': {
type: 'error.platform.RevokeVids.requestingRevoke:invocation[0]';
data: unknown;
};
'xstate.init': { type: 'xstate.init' };
};
'invokeSrcNameMap': {
requestOtp: 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]';
requestRevoke: 'done.invoke.RevokeVids.requestingRevoke:invocation[0]';
};
'missingImplementations': {
actions: never;
services: never;
guards: never;
delays: never;
};
'eventsCausingActions': {
clearOtp:
| 'DISMISS'
| 'REVOKE_VCS'
| 'done.invoke.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]'
| 'error.platform.RevokeVids.requestingRevoke:invocation[0]'
| 'xstate.init';
logRevoked: 'STORE_RESPONSE';
revokeVID: 'done.invoke.RevokeVids.requestingRevoke:invocation[0]';
setIdBackendError: 'error.platform.RevokeVids.acceptingVIDs.requestingOtp:invocation[0]';
setOtp: 'INPUT_OTP';
setOtpError: 'error.platform.RevokeVids.requestingRevoke:invocation[0]';
setTransactionId: 'DISMISS' | 'REVOKE_VCS' | 'xstate.init';
setVIDs: 'REVOKE_VCS';
};
'eventsCausingServices': {
requestOtp: never;
requestRevoke: 'INPUT_OTP';
};
'eventsCausingGuards': {};
'eventsCausingDelays': {};
'matchesStates':
| 'acceptingOtpInput'
| 'acceptingVIDs'
| 'acceptingVIDs.idle'
| 'acceptingVIDs.requestingOtp'
| 'idle'
| 'invalid'
| 'invalid.backend'
| 'invalid.otp'
| 'loggingRevoke'
| 'requestingRevoke'
| 'revokingVc'
| { acceptingVIDs?: 'idle' | 'requestingOtp'; invalid?: 'backend' | 'otp' };
'tags': never;
}

View File

@@ -1,119 +0,0 @@
import RNSafetyNetClient from '@bitwala/react-native-safetynet';
import { getDeviceId } from 'react-native-device-info';
import { ContextFrom } from 'xstate';
import { createModel } from 'xstate/lib/model';
const ATTESTATION_API_KEY = 'YOUR API KEY';
const ATTESTATION_ENDPOINT = '';
const NONCE_ENDPOINT = '';
const model = createModel(
{
nonce: '',
jws: '',
error: '',
},
{
events: {
NONCE_RECEIVED: (nonce: string) => ({ nonce }),
ATTESTATION_RECEIVED: (jws: string) => ({ jws }),
ERROR: (error: string) => ({ error }),
VERIFIED: () => ({}),
},
}
);
type Context = ContextFrom<typeof model>;
export const safetynetMachine = model.createMachine(
{
id: 'safetynet',
context: model.initialContext,
initial: 'requestingAttestation', // 'requestingNonce',
states: {
requestingNonce: {
invoke: {
src: 'requestNonce',
},
},
requestingAttestation: {
invoke: {
src: 'requestAttestation',
onDone: '',
},
},
verifyingAttestation: {
invoke: {
src: 'verifyAttestation',
},
},
verified: {
type: 'final',
data: {
jws: (context: Context) => context.jws,
},
},
failed: {
type: 'final',
data: {
error: (context: Context) => context.error,
},
},
},
},
{
actions: {},
services: {
requestNonce: () => async (callback) => {
const nonceResult = await RNSafetyNetClient.requestNonce({
endPointUrl: NONCE_ENDPOINT,
additionalData: getDeviceId(),
});
if (!nonceResult.nonce || nonceResult.error) {
callback(
model.events.ERROR(nonceResult.error || 'Nonce request failed.')
);
} else {
callback(model.events.NONCE_RECEIVED(nonceResult.nonce));
}
},
requestAttestation: (context) => async (callback) => {
const attestationResult =
await RNSafetyNetClient.sendAttestationRequest(
context.nonce,
ATTESTATION_API_KEY
);
if (!attestationResult.jws || attestationResult.error) {
callback(
model.events.ERROR(
attestationResult.error || 'Attestation request failed.'
)
);
} else {
callback(model.events.ATTESTATION_RECEIVED(attestationResult.jws));
}
},
verifyAttestation: (context) => async (callback) => {
const verification = (await RNSafetyNetClient.verifyAttestationResult({
endPointUrl: ATTESTATION_ENDPOINT,
attestationJws: context.jws,
})) as VerificationResult;
if (!verification.success) {
callback(model.events.ERROR('Verfication failed.'));
} else {
callback(model.events.VERIFIED());
}
},
},
}
);
interface VerificationResult {
success: boolean;
}

View File

@@ -2,15 +2,12 @@ import SmartshareReactNative from '@idpass/smartshare-react-native';
import { ConnectionParams } from '@idpass/smartshare-react-native/lib/typescript/IdpassSmartshare';
const { IdpassSmartshare, GoogleNearbyMessages } = SmartshareReactNative;
// import LocationEnabler from 'react-native-location-enabler';
const LocationEnabler = {} as any;
import SystemSetting from 'react-native-system-setting';
import { assign, EventFrom, send, sendParent, StateFrom } from 'xstate';
import { createModel } from 'xstate/lib/model';
import { EmitterSubscription, Linking, Platform } from 'react-native';
import { DeviceInfo } from '../components/DeviceInfoList';
import { getDeviceNameSync } from 'react-native-device-info';
import { VC } from '../types/vc';
import { VC, VerifiablePresentation } from '../types/vc';
import { AppServices } from '../shared/GlobalContext';
import { ActivityLogEvents } from './activityLog';
import {
@@ -29,10 +26,11 @@ import {
SendVcStatus,
} from '../shared/smartshare';
import { check, PERMISSIONS, PermissionStatus } from 'react-native-permissions';
import { checkLocation, requestLocation } from '../shared/location';
import { CameraCapturedPicture } from 'expo-camera';
const checkingAirplaneMode = '#checkingAirplaneMode';
const checkingLocationService = '#checkingLocationService';
const findingConnection = '#scan.findingConnection';
const findingConnectionId = '#scan.findingConnection';
const checkingLocationServiceId = '#checkingLocationService';
type SharingProtocol = 'OFFLINE' | 'ONLINE';
@@ -46,12 +44,8 @@ const model = createModel(
selectedVc: {} as VC,
reason: '',
loggers: [] as EmitterSubscription[],
locationConfig: {
// priority: LocationEnabler.PRIORITIES.BALANCED_POWER_ACCURACY,
alwaysShow: false,
needBle: true,
},
vcName: '',
verificationImage: {} as CameraCapturedPicture,
sharingProtocol: 'OFFLINE' as SharingProtocol,
scannedQrParams: {} as ConnectionParams,
},
@@ -73,13 +67,15 @@ const model = createModel(
UPDATE_REASON: (reason: string) => ({ reason }),
LOCATION_ENABLED: () => ({}),
LOCATION_DISABLED: () => ({}),
FLIGHT_ENABLED: () => ({}),
FLIGHT_DISABLED: () => ({}),
FLIGHT_REQUEST: () => ({}),
LOCATION_REQUEST: () => ({}),
UPDATE_VC_NAME: (vcName: string) => ({ vcName }),
STORE_RESPONSE: (response: unknown) => ({ response }),
APP_ACTIVE: () => ({}),
VERIFY_AND_SELECT_VC: (vc: VC) => ({ vc }),
FACE_VALID: () => ({}),
FACE_INVALID: () => ({}),
RETRY_VERIFICATION: () => ({}),
VP_CREATED: (vp: VerifiablePresentation) => ({ vp }),
},
}
);
@@ -100,42 +96,12 @@ export const scanMachine = model.createMachine(
},
on: {
SCREEN_BLUR: 'inactive',
SCREEN_FOCUS: 'checkingAirplaneMode',
SCREEN_FOCUS: 'checkingLocationService',
},
states: {
inactive: {
entry: ['removeLoggers'],
},
checkingAirplaneMode: {
id: 'checkingAirplaneMode',
on: {
APP_ACTIVE: '.checkingStatus',
},
initial: 'checkingStatus',
states: {
checkingStatus: {
invoke: {
src: 'checkAirplaneMode',
},
on: {
FLIGHT_DISABLED: checkingLocationService,
FLIGHT_ENABLED: 'enabled',
},
},
requestingToDisable: {
entry: ['requestToDisableFlightMode'],
on: {
FLIGHT_DISABLED: checkingLocationService,
},
},
enabled: {
on: {
FLIGHT_REQUEST: 'requestingToDisable',
FLIGHT_DISABLED: checkingLocationService,
},
},
},
},
checkingLocationService: {
id: 'checkingLocationService',
invoke: {
@@ -147,7 +113,6 @@ export const scanMachine = model.createMachine(
on: {
LOCATION_ENABLED: 'checkingPermission',
LOCATION_DISABLED: 'requestingToEnable',
FLIGHT_ENABLED: checkingAirplaneMode,
},
},
requestingToEnable: {
@@ -205,7 +170,6 @@ export const scanMachine = model.createMachine(
},
{ target: 'invalid' },
],
FLIGHT_ENABLED: checkingAirplaneMode,
},
},
preparingToConnect: {
@@ -224,6 +188,23 @@ export const scanMachine = model.createMachine(
on: {
CONNECTED: 'exchangingDeviceInfo',
},
initial: 'inProgress',
states: {
inProgress: {},
timeout: {
on: {
CANCEL: {
actions: 'disconnect',
target: checkingLocationServiceId,
},
},
},
},
after: {
CONNECTION_TIMEOUT: {
target: '.timeout',
},
},
},
exchangingDeviceInfo: {
invoke: {
@@ -236,6 +217,23 @@ export const scanMachine = model.createMachine(
actions: ['setReceiverInfo'],
},
},
initial: 'inProgress',
states: {
inProgress: {},
timeout: {
on: {
CANCEL: {
actions: 'disconnect',
target: checkingLocationServiceId,
},
},
},
},
after: {
CONNECTION_TIMEOUT: {
target: '.timeout',
},
},
},
reviewing: {
on: {
@@ -251,16 +249,20 @@ export const scanMachine = model.createMachine(
idle: {
on: {
ACCEPT_REQUEST: 'selectingVc',
DISCONNECT: findingConnection,
DISCONNECT: findingConnectionId,
},
},
selectingVc: {
on: {
DISCONNECT: findingConnection,
DISCONNECT: findingConnectionId,
SELECT_VC: {
target: 'sendingVc',
actions: ['setSelectedVc'],
},
VERIFY_AND_SELECT_VC: {
target: 'verifyingUserIdentity',
actions: ['setSelectedVc'],
},
CANCEL: 'idle',
},
},
@@ -269,10 +271,27 @@ export const scanMachine = model.createMachine(
src: 'sendVc',
},
on: {
DISCONNECT: findingConnection,
DISCONNECT: findingConnectionId,
VC_ACCEPTED: 'accepted',
VC_REJECTED: 'rejected',
},
initial: 'inProgress',
states: {
inProgress: {},
timeout: {
on: {
CANCEL: {
actions: 'disconnect',
target: checkingLocationServiceId,
},
},
},
},
after: {
CONNECTION_TIMEOUT: {
target: '.timeout',
},
},
},
accepted: {
entry: ['logShared'],
@@ -283,10 +302,28 @@ export const scanMachine = model.createMachine(
rejected: {},
cancelled: {},
navigatingToHome: {},
verifyingUserIdentity: {
on: {
FACE_VALID: {
target: 'sendingVc',
},
FACE_INVALID: {
target: 'invalidUserIdentity',
},
CANCEL: 'selectingVc',
},
},
invalidUserIdentity: {
on: {
DISMISS: 'selectingVc',
RETRY_VERIFICATION: 'verifyingUserIdentity',
},
},
},
exit: ['disconnect', 'clearReason'],
},
disconnected: {
id: 'disconnected',
on: {
DISMISS: 'findingConnection',
},
@@ -306,17 +343,7 @@ export const scanMachine = model.createMachine(
senderInfo: (_, event) => event.info,
}),
requestToEnableLocation: (context) => {
LocationEnabler?.requestResolutionSettings(context.locationConfig);
},
requestToDisableFlightMode: () => {
if (Platform.OS === 'android') {
SystemSetting.switchAirplane();
} else {
Linking.openURL('App-prefs:root=AIRPLANE_MODE');
}
},
requestToEnableLocation: () => requestLocation(),
disconnect: (context) => {
try {
@@ -409,9 +436,7 @@ export const scanMachine = model.createMachine(
{ to: (context) => context.serviceRefs.activityLog }
),
openSettings: () => {
Linking.openSettings();
},
openSettings: () => Linking.openSettings(),
},
services: {
@@ -424,22 +449,9 @@ export const scanMachine = model.createMachine(
if (Platform.OS === 'android') {
response = await check(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
} else if (Platform.OS === 'ios') {
callback(model.events.LOCATION_ENABLED());
return;
// response = await check(PERMISSIONS.IOS.LOCATION_WHEN_IN_USE);
return callback(model.events.LOCATION_ENABLED());
}
// const response = await PermissionsAndroid.request(
// PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
// {
// title: 'Location access',
// message:
// 'Location access is required for the scanning functionality.',
// buttonNegative: 'Cancel',
// buttonPositive: 'OK',
// }
// );
if (response === 'granted') {
callback(model.events.LOCATION_ENABLED());
} else {
@@ -463,26 +475,10 @@ export const scanMachine = model.createMachine(
},
checkLocationStatus: () => (callback) => {
// const listener = LocationEnabler.addListener(({ locationEnabled }) => {
// if (locationEnabled) {
// callback(model.events.LOCATION_ENABLED());
// } else {
// callback(model.events.LOCATION_DISABLED());
// }
// });
// LocationEnabler.checkSettings(context.locationConfig);
// return () => listener.remove();
callback(model.events.LOCATION_ENABLED());
},
checkAirplaneMode: () => (callback) => {
SystemSetting.isAirplaneEnabled().then((enable) => {
if (enable) {
callback(model.events.FLIGHT_ENABLED());
} else {
callback(model.events.FLIGHT_DISABLED());
}
});
checkLocation(
() => callback(model.events.LOCATION_ENABLED()),
() => callback(model.events.LOCATION_DISABLED())
);
},
discoverDevice: (context) => (callback) => {
@@ -563,7 +559,6 @@ export const scanMachine = model.createMachine(
};
if (context.sharingProtocol === 'OFFLINE') {
console.log('OFFLINE?!');
const event: SendVcEvent = {
type: 'send-vc',
data: { isChunked: false, vc },
@@ -604,6 +599,9 @@ export const scanMachine = model.createMachine(
delays: {
CLEAR_DELAY: 250,
CONNECTION_TIMEOUT: () => {
return (Platform.OS === 'ios' ? 15 : 5) * 1000;
},
},
}
);
@@ -629,16 +627,28 @@ export function selectVcName(state: State) {
return state.context.vcName;
}
export function selectSelectedVc(state: State) {
return state.context.selectedVc;
}
export function selectIsScanning(state: State) {
return state.matches('findingConnection');
}
export function selectIsConnecting(state: State) {
return state.matches('connecting');
return state.matches('connecting.inProgress');
}
export function selectIsConnectingTimeout(state: State) {
return state.matches('connecting.timeout');
}
export function selectIsExchangingDeviceInfo(state: State) {
return state.matches('exchangingDeviceInfo');
return state.matches('exchangingDeviceInfo.inProgress');
}
export function selectIsExchangingDeviceInfoTimeout(state: State) {
return state.matches('exchangingDeviceInfo.timeout');
}
export function selectIsReviewing(state: State) {
@@ -650,7 +660,11 @@ export function selectIsSelectingVc(state: State) {
}
export function selectIsSendingVc(state: State) {
return state.matches('reviewing.sendingVc');
return state.matches('reviewing.sendingVc.inProgress');
}
export function selectIsSendingVcTimeout(state: State) {
return state.matches('reviewing.sendingVc.timeout');
}
export function selectIsAccepted(state: State) {
@@ -673,15 +687,22 @@ export function selectIsLocationDisabled(state: State) {
return state.matches('checkingLocationService.disabled');
}
export function selectIsAirplaneEnabled(state: State) {
return state.matches('checkingAirplaneMode.enabled');
export function selectIsDone(state: State) {
return state.matches('reviewing.navigatingToHome');
}
export function selectIsVerifyingUserIdentity(state: State) {
return state.matches('reviewing.verifyingUserIdentity');
}
export function selectIsInvalidUserIdentity(state: State) {
return state.matches('reviewing.invalidUserIdentity');
}
async function sendVc(vc: VC, callback: (status: SendVcStatus) => void) {
const rawData = JSON.stringify(vc);
const chunks = chunkString(rawData, GNM_MESSAGE_LIMIT);
if (chunks.length > 1) {
console.log('CHUNKED!', chunks.length);
let chunk = 0;
const vcChunk = {
total: chunks.length,
@@ -700,7 +721,6 @@ async function sendVc(vc: VC, callback: (status: SendVcStatus) => void) {
SendVcResponseType,
async (status) => {
if (typeof status === 'number' && chunk < event.data.vcChunk.total) {
console.log(SendVcResponseType, chunk, chunks[chunk].length);
chunk += 1;
await GoogleNearbyMessages.unpublish();
await onlineSend({
@@ -723,7 +743,6 @@ async function sendVc(vc: VC, callback: (status: SendVcStatus) => void) {
);
await onlineSend(event);
} else {
console.log('UNCHUNKED');
const event: SendVcEvent = {
type: 'send-vc',
data: { isChunked: false, vc },

View File

@@ -10,7 +10,6 @@ export interface Typegen0 {
'xstate.stop': { type: 'xstate.stop' };
};
'invokeSrcNameMap': {
checkAirplaneMode: 'done.invoke.scan.checkingAirplaneMode.checkingStatus:invocation[0]';
checkLocationPermission: 'done.invoke.scan.checkingLocationService.checkingPermission:invocation[0]';
checkLocationStatus: 'done.invoke.checkingLocationService:invocation[0]';
discoverDevice: 'done.invoke.scan.connecting:invocation[0]';
@@ -61,23 +60,21 @@ export interface Typegen0 {
| 'xstate.after(CLEAR_DELAY)#clearingConnection'
| 'xstate.init';
requestSenderInfo: 'SCAN';
requestToDisableFlightMode: 'FLIGHT_REQUEST';
requestToEnableLocation: 'LOCATION_DISABLED' | 'LOCATION_REQUEST';
setConnectionParams: 'SCAN';
setReason: 'UPDATE_REASON';
setReceiverInfo: 'EXCHANGE_DONE';
setScannedQrParams: 'SCAN';
setSelectedVc: 'SELECT_VC';
setSelectedVc: 'SELECT_VC' | 'VERIFY_AND_SELECT_VC';
setSenderInfo: 'RECEIVE_DEVICE_INFO';
};
'eventsCausingServices': {
checkAirplaneMode: 'APP_ACTIVE' | 'FLIGHT_ENABLED' | 'SCREEN_FOCUS';
checkLocationPermission: 'APP_ACTIVE' | 'LOCATION_ENABLED';
checkLocationStatus: 'FLIGHT_DISABLED';
checkLocationStatus: 'CANCEL' | 'SCREEN_FOCUS';
discoverDevice: 'RECEIVE_DEVICE_INFO';
exchangeDeviceInfo: 'CONNECTED';
monitorConnection: 'SCREEN_BLUR' | 'SCREEN_FOCUS' | 'xstate.init';
sendVc: 'SELECT_VC';
sendVc: 'FACE_VALID' | 'SELECT_VC';
};
'eventsCausingGuards': {
isQrOffline: 'SCAN';
@@ -85,12 +82,13 @@ export interface Typegen0 {
};
'eventsCausingDelays': {
CLEAR_DELAY: 'LOCATION_ENABLED';
CONNECTION_TIMEOUT:
| 'CONNECTED'
| 'FACE_VALID'
| 'RECEIVE_DEVICE_INFO'
| 'SELECT_VC';
};
'matchesStates':
| 'checkingAirplaneMode'
| 'checkingAirplaneMode.checkingStatus'
| 'checkingAirplaneMode.enabled'
| 'checkingAirplaneMode.requestingToDisable'
| 'checkingLocationService'
| 'checkingLocationService.checkingPermission'
| 'checkingLocationService.checkingStatus'
@@ -99,8 +97,12 @@ export interface Typegen0 {
| 'checkingLocationService.requestingToEnable'
| 'clearingConnection'
| 'connecting'
| 'connecting.inProgress'
| 'connecting.timeout'
| 'disconnected'
| 'exchangingDeviceInfo'
| 'exchangingDeviceInfo.inProgress'
| 'exchangingDeviceInfo.timeout'
| 'findingConnection'
| 'inactive'
| 'invalid'
@@ -109,29 +111,34 @@ export interface Typegen0 {
| 'reviewing.accepted'
| 'reviewing.cancelled'
| 'reviewing.idle'
| 'reviewing.invalidUserIdentity'
| 'reviewing.navigatingToHome'
| 'reviewing.rejected'
| 'reviewing.selectingVc'
| 'reviewing.sendingVc'
| 'reviewing.sendingVc.inProgress'
| 'reviewing.sendingVc.timeout'
| 'reviewing.verifyingUserIdentity'
| {
checkingAirplaneMode?:
| 'checkingStatus'
| 'enabled'
| 'requestingToDisable';
checkingLocationService?:
| 'checkingPermission'
| 'checkingStatus'
| 'denied'
| 'disabled'
| 'requestingToEnable';
connecting?: 'inProgress' | 'timeout';
exchangingDeviceInfo?: 'inProgress' | 'timeout';
reviewing?:
| 'accepted'
| 'cancelled'
| 'idle'
| 'invalidUserIdentity'
| 'navigatingToHome'
| 'rejected'
| 'selectingVc'
| 'sendingVc';
| 'sendingVc'
| 'verifyingUserIdentity'
| { sendingVc?: 'inProgress' | 'timeout' };
};
'tags': never;
}

View File

@@ -2,18 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
setContext: 'STORE_RESPONSE';
toggleBiometricUnlock: 'TOGGLE_BIOMETRIC_UNLOCK';
storeContext:
| 'TOGGLE_BIOMETRIC_UNLOCK'
| 'UPDATE_NAME'
| 'UPDATE_VC_LABEL'
| 'STORE_RESPONSE';
updateName: 'UPDATE_NAME';
updateVcLabel: 'UPDATE_VC_LABEL';
requestStoredContext: 'xstate.init';
};
'internalEvents': {
'xstate.init': { type: 'xstate.init' };
};
@@ -24,11 +12,23 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
requestStoredContext: 'xstate.init';
setContext: 'STORE_RESPONSE';
storeContext:
| 'STORE_RESPONSE'
| 'TOGGLE_BIOMETRIC_UNLOCK'
| 'UPDATE_NAME'
| 'UPDATE_VC_LABEL';
toggleBiometricUnlock: 'TOGGLE_BIOMETRIC_UNLOCK';
updateName: 'UPDATE_NAME';
updateVcLabel: 'UPDATE_VC_LABEL';
};
'eventsCausingServices': {};
'eventsCausingGuards': {
hasData: 'STORE_RESPONSE';
};
'eventsCausingDelays': {};
'matchesStates': 'init' | 'storingDefaults' | 'idle';
'matchesStates': 'idle' | 'init' | 'storingDefaults';
'tags': never;
}

View File

@@ -21,7 +21,8 @@ const model = createModel(
SET: (key: string, value: unknown) => ({ key, value }),
APPEND: (key: string, value: unknown) => ({ key, value }),
PREPEND: (key: string, value: unknown) => ({ key, value }),
REMOVE: (key: string) => ({ key }),
REMOVE: (key: string, value: string) => ({ key, value }),
REMOVE_ITEMS: (key: string, values: string[]) => ({ key, values }),
CLEAR: () => ({}),
ERROR: (error: Error) => ({ error }),
STORE_RESPONSE: (response?: unknown, requester?: string) => ({
@@ -116,6 +117,9 @@ export const storeMachine =
REMOVE: {
actions: 'forwardStoreRequest',
},
REMOVE_ITEMS: {
actions: 'forwardStoreRequest',
},
CLEAR: {
actions: 'forwardStoreRequest',
},
@@ -200,7 +204,21 @@ export const storeMachine =
break;
}
case 'REMOVE': {
await removeItem(event.key);
await removeItem(
event.key,
event.value,
context.encryptionKey
);
response = event.value;
break;
}
case 'REMOVE_ITEMS': {
await removeItems(
event.key,
event.values,
context.encryptionKey
);
response = event.values;
break;
}
case 'CLEAR': {
@@ -280,6 +298,7 @@ export async function getItem(
const data = await AsyncStorage.getItem(key);
if (data != null) {
const decrypted = decryptJson(encryptionKey, data);
return JSON.parse(decrypted);
} else {
return defaultValue;
@@ -310,23 +329,67 @@ export async function prependItem(
) {
try {
const list = await getItem(key, [], encryptionKey);
const newList = Array.isArray(value)
? [...value, ...list]
: [value, ...list];
await setItem(key, [value, ...list], encryptionKey);
await setItem(key, newList, encryptionKey);
} catch (e) {
console.error('error prependItem:', e);
throw e;
}
}
export async function removeItem(key: string) {
export async function removeItem(
key: string,
value: string,
encryptionKey: string
) {
try {
await AsyncStorage.removeItem(key);
const data = await AsyncStorage.getItem(key);
const decrypted = decryptJson(encryptionKey, data);
const list = JSON.parse(decrypted);
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);
});
await setItem(key, newList, encryptionKey);
} catch (e) {
console.error('error removeItem:', e);
throw e;
}
}
export async function removeItems(
key: string,
values: string[],
encryptionKey: string
) {
try {
const data = await AsyncStorage.getItem(key);
const decrypted = decryptJson(encryptionKey, data);
const list = JSON.parse(decrypted);
const newList = list.filter(function (vc: string) {
return !values.find(function (vcKey: string) {
const vcKeyArray = vcKey.split(':');
const finalVcKeyArray = vcKeyArray.pop();
console.log('finalVcKeyArray', finalVcKeyArray);
const finalVcKey = vcKeyArray.join(':');
return vc.includes(finalVcKey);
});
});
await setItem(key, newList, encryptionKey);
} catch (e) {
console.error('error removeItems:', e);
throw e;
}
}
export async function clear() {
try {
await AsyncStorage.clear();

View File

@@ -3,27 +3,27 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
'error.platform.store.resettingStorage:invocation[0]': {
type: 'error.platform.store.resettingStorage:invocation[0]';
'done.invoke._store': {
type: 'done.invoke._store';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.store.resettingStorage:invocation[0]': {
type: 'done.invoke.store.resettingStorage:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'xstate.init': { type: 'xstate.init' };
'done.invoke._store': {
type: 'done.invoke._store';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform._store': { type: 'error.platform._store'; data: unknown };
'error.platform.store.resettingStorage:invocation[0]': {
type: 'error.platform.store.resettingStorage:invocation[0]';
data: unknown;
};
'xstate.init': { type: 'xstate.init' };
};
'invokeSrcNameMap': {
getEncryptionKey: 'done.invoke.store.gettingEncryptionKey:invocation[0]';
generateEncryptionKey: 'done.invoke.store.generatingEncryptionKey:invocation[0]';
clear: 'done.invoke.store.resettingStorage:invocation[0]';
generateEncryptionKey: 'done.invoke.store.generatingEncryptionKey:invocation[0]';
getEncryptionKey: 'done.invoke.store.gettingEncryptionKey:invocation[0]';
store: 'done.invoke._store';
};
'missingImplementations': {
@@ -33,30 +33,31 @@ export interface Typegen0 {
delays: never;
};
'eventsCausingActions': {
setEncryptionKey: 'KEY_RECEIVED';
forwardStoreRequest:
| 'GET'
| 'SET'
| 'APPEND'
| 'CLEAR'
| 'GET'
| 'PREPEND'
| 'REMOVE'
| 'CLEAR';
| 'REMOVE_ITEMS'
| 'SET';
notifyParent:
| 'KEY_RECEIVED'
| 'done.invoke.store.resettingStorage:invocation[0]';
setEncryptionKey: 'KEY_RECEIVED';
};
'eventsCausingServices': {
getEncryptionKey: 'xstate.init';
generateEncryptionKey: 'ERROR';
clear: 'KEY_RECEIVED';
generateEncryptionKey: 'ERROR';
getEncryptionKey: 'xstate.init';
store: 'KEY_RECEIVED' | 'done.invoke.store.resettingStorage:invocation[0]';
};
'eventsCausingGuards': {};
'eventsCausingDelays': {};
'matchesStates':
| 'gettingEncryptionKey'
| 'generatingEncryptionKey'
| 'resettingStorage'
| 'ready';
| 'gettingEncryptionKey'
| 'ready'
| 'resettingStorage';
'tags': never;
}

View File

@@ -4,7 +4,7 @@ import { createModel } from 'xstate/lib/model';
import { StoreEvents } from './store';
import { VC } from '../types/vc';
import { AppServices } from '../shared/GlobalContext';
import { respond } from 'xstate/lib/actions';
import { log, respond } from 'xstate/lib/actions';
import { VcItemEvents } from './vcItem';
import {
MY_VCS_STORE_KEY,
@@ -29,6 +29,7 @@ const model = createModel(
VC_RECEIVED: (vcKey: string) => ({ vcKey }),
VC_DOWNLOADED: (vc: VC) => ({ vc }),
REFRESH_MY_VCS: () => ({}),
REFRESH_MY_VCS_TWO: (vc: VC) => ({ vc }),
REFRESH_RECEIVED_VCS: () => ({}),
GET_RECEIVED_VCS: () => ({}),
},
@@ -82,6 +83,7 @@ export const vcMachine =
idle: {
on: {
REFRESH_MY_VCS: {
actions: [log('REFRESH_MY_VCS:myVcs---')],
target: 'refreshing',
},
},

View File

@@ -2,18 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
setMyVcs: 'STORE_RESPONSE';
setReceivedVcs: 'STORE_RESPONSE';
getReceivedVcsResponse: 'GET_RECEIVED_VCS';
getVcItemResponse: 'GET_VC_ITEM';
prependToMyVcs: 'VC_ADDED';
setDownloadedVc: 'VC_DOWNLOADED';
moveExistingVcToTop: 'VC_RECEIVED';
prependToReceivedVcs: 'VC_RECEIVED';
loadMyVcs: 'REFRESH_MY_VCS';
loadReceivedVcs: 'STORE_RESPONSE' | 'REFRESH_RECEIVED_VCS';
};
'internalEvents': {
'xstate.init': { type: 'xstate.init' };
};
@@ -24,6 +12,18 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
getReceivedVcsResponse: 'GET_RECEIVED_VCS';
getVcItemResponse: 'GET_VC_ITEM';
loadMyVcs: 'REFRESH_MY_VCS' | 'xstate.init';
loadReceivedVcs: 'REFRESH_RECEIVED_VCS' | 'STORE_RESPONSE';
moveExistingVcToTop: 'VC_RECEIVED';
prependToMyVcs: 'VC_ADDED';
prependToReceivedVcs: 'VC_RECEIVED';
setDownloadedVc: 'VC_DOWNLOADED';
setMyVcs: 'STORE_RESPONSE';
setReceivedVcs: 'STORE_RESPONSE';
};
'eventsCausingServices': {};
'eventsCausingGuards': {
hasExistingReceivedVc: 'VC_RECEIVED';

View File

@@ -1,6 +1,6 @@
import { assign, ErrorPlatformEvent, EventFrom, send, StateFrom } from 'xstate';
import { createModel } from 'xstate/lib/model';
import { VC_ITEM_STORE_KEY } from '../shared/constants';
import { MY_VCS_STORE_KEY, VC_ITEM_STORE_KEY } from '../shared/constants';
import { AppServices } from '../shared/GlobalContext';
import { CredentialDownloadResponse, request } from '../shared/request';
import {
@@ -11,7 +11,8 @@ import {
} from '../types/vc';
import { StoreEvents } from './store';
import { ActivityLogEvents } from './activityLog';
import { verifyCredential } from '../shared/verifyCredential';
import { verifyCredential } from '../shared/vcjs/verifyCredential';
import { log } from 'xstate/lib/actions';
const model = createModel(
{
@@ -30,6 +31,7 @@ const model = createModel(
otpError: '',
idError: '',
transactionId: '',
revoked: false,
},
{
events: {
@@ -44,9 +46,9 @@ const model = createModel(
GET_VC_RESPONSE: (vc: VC) => ({ vc }),
VERIFY: () => ({}),
LOCK_VC: () => ({}),
UNLOCK_VC: () => ({}),
INPUT_OTP: (otp: string) => ({ otp }),
REFRESH: () => ({}),
REVOKE_VC: () => ({}),
},
}
);
@@ -157,8 +159,8 @@ export const vcItemMachine =
LOCK_VC: {
target: 'requestingOtp',
},
UNLOCK_VC: {
target: 'requestingOtp',
REVOKE_VC: {
target: 'acceptingRevokeInput',
},
},
},
@@ -228,28 +230,54 @@ export const vcItemMachine =
},
},
requestingOtp: {
entry: 'setTransactionId',
invoke: {
src: 'requestOtp',
onDone: [
{
actions: [log('accepting OTP')],
target: 'acceptingOtpInput',
},
],
onError: [
{
actions: [log('error OTP')],
target: '#vc-item.invalid.backend',
},
],
},
},
acceptingOtpInput: {
entry: 'clearOtp',
entry: ['clearOtp', 'setTransactionId'],
on: {
INPUT_OTP: {
actions: 'setOtp',
target: 'requestingLock',
INPUT_OTP: [
{
actions: [
log('setting OTP lock'),
'setTransactionId',
'setOtp',
],
target: 'requestingLock',
},
],
DISMISS: {
actions: ['clearOtp', 'clearTransactionId'],
target: 'idle',
},
},
},
acceptingRevokeInput: {
entry: [log('acceptingRevokeInput'), 'clearOtp', 'setTransactionId'],
on: {
INPUT_OTP: [
{
actions: [
log('setting OTP revoke'),
'setTransactionId',
'setOtp',
],
target: 'requestingRevoke',
},
],
DISMISS: {
actions: ['clearOtp', 'clearTransactionId'],
target: 'idle',
@@ -274,13 +302,46 @@ export const vcItemMachine =
},
},
lockingVc: {
entry: 'storeLock',
entry: ['storeLock'],
on: {
STORE_RESPONSE: {
target: 'idle',
},
},
},
requestingRevoke: {
invoke: {
src: 'requestRevoke',
onDone: [
{
actions: [log('doneRevoking'), 'setRevoke'],
target: 'revokingVc',
},
],
onError: [
{
actions: [log('OTP error'), 'setOtpError'],
target: 'acceptingOtpInput',
},
],
},
},
revokingVc: {
entry: ['revokeVID'],
on: {
STORE_RESPONSE: {
target: 'loggingRevoke',
},
},
},
loggingRevoke: {
entry: [log('loggingRevoke'), 'logRevoked'],
on: {
DISMISS: {
target: 'idle',
},
},
},
},
},
{
@@ -356,6 +417,32 @@ export const vcItemMachine =
}
),
logRevoked: send(
(context) =>
ActivityLogEvents.LOG_ACTIVITY({
_vcKey: VC_ITEM_STORE_KEY(context),
action: 'revoked',
timestamp: Date.now(),
deviceName: '',
vcLabel: context.tag || context.id,
}),
{
to: (context) => context.serviceRefs.activityLog,
}
),
revokeVID: send(
(context) => {
return StoreEvents.REMOVE(
MY_VCS_STORE_KEY,
VC_ITEM_STORE_KEY(context)
);
},
{
to: (context) => context.serviceRefs.store,
}
),
markVcValid: assign((context) => {
return {
...context,
@@ -385,6 +472,10 @@ export const vcItemMachine =
locked: (context) => !context.locked,
}),
setRevoke: assign({
revoked: () => true,
}),
storeLock: send(
(context) => {
const { serviceRefs, ...data } = context;
@@ -497,6 +588,20 @@ export const vcItemMachine =
}
return response.response;
},
requestRevoke: async (context) => {
try {
return request('PATCH', `/vid/${context.id}`, {
transactionID: context.transactionId,
vidStatus: 'REVOKED',
individualId: context.id,
individualIdType: 'VID',
otp: context.otp,
});
} catch (error) {
console.error(error);
}
},
},
guards: {
@@ -579,10 +684,22 @@ export function selectIsLockingVc(state: State) {
return state.matches('lockingVc');
}
export function selectIsRevokingVc(state: State) {
return state.matches('revokingVc');
}
export function selectIsLoggingRevoke(state: State) {
return state.matches('loggingRevoke');
}
export function selectIsAcceptingOtpInput(state: State) {
return state.matches('acceptingOtpInput');
}
export function selectIsAcceptingRevokeInput(state: State) {
return state.matches('acceptingRevokeInput');
}
export function selectIsRequestingOtp(state: State) {
return state.matches('requestingOtp');
}

View File

@@ -24,6 +24,11 @@ export interface Typegen0 {
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.vc-item.requestingRevoke:invocation[0]': {
type: 'done.invoke.vc-item.requestingRevoke:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.vc-item.verifyingCredential:invocation[0]': {
type: 'done.invoke.vc-item.verifyingCredential:invocation[0]';
data: unknown;
@@ -41,6 +46,14 @@ export interface Typegen0 {
type: 'error.platform.vc-item.requestingLock:invocation[0]';
data: unknown;
};
'error.platform.vc-item.requestingOtp:invocation[0]': {
type: 'error.platform.vc-item.requestingOtp:invocation[0]';
data: unknown;
};
'error.platform.vc-item.requestingRevoke:invocation[0]': {
type: 'error.platform.vc-item.requestingRevoke:invocation[0]';
data: unknown;
};
'error.platform.vc-item.verifyingCredential:invocation[0]': {
type: 'error.platform.vc-item.verifyingCredential:invocation[0]';
data: unknown;
@@ -52,6 +65,7 @@ export interface Typegen0 {
downloadCredential: 'done.invoke.downloadCredential';
requestLock: 'done.invoke.vc-item.requestingLock:invocation[0]';
requestOtp: 'done.invoke.vc-item.requestingOtp:invocation[0]';
requestRevoke: 'done.invoke.vc-item.requestingRevoke:invocation[0]';
verifyCredential: 'done.invoke.vc-item.verifyingCredential:invocation[0]';
};
'missingImplementations': {
@@ -64,10 +78,12 @@ export interface Typegen0 {
clearOtp:
| ''
| 'DISMISS'
| 'REVOKE_VC'
| 'STORE_RESPONSE'
| 'done.invoke.vc-item.requestingOtp:invocation[0]'
| 'done.invoke.vc-item.verifyingCredential:invocation[0]'
| 'error.platform.vc-item.requestingLock:invocation[0]'
| 'error.platform.vc-item.requestingRevoke:invocation[0]'
| 'error.platform.vc-item.verifyingCredential:invocation[0]';
clearTransactionId:
| ''
@@ -77,18 +93,28 @@ export interface Typegen0 {
| 'error.platform.vc-item.verifyingCredential:invocation[0]';
logDownloaded: 'CREDENTIAL_DOWNLOADED';
logError: 'error.platform.vc-item.verifyingCredential:invocation[0]';
logRevoked: 'STORE_RESPONSE';
markVcValid: 'done.invoke.vc-item.verifyingCredential:invocation[0]';
requestStoredContext: 'GET_VC_RESPONSE' | 'REFRESH';
requestVcContext: 'xstate.init';
revokeVID: 'done.invoke.vc-item.requestingRevoke:invocation[0]';
setCredential:
| 'CREDENTIAL_DOWNLOADED'
| 'GET_VC_RESPONSE'
| 'STORE_RESPONSE';
setLock: 'done.invoke.vc-item.requestingLock:invocation[0]';
setOtp: 'INPUT_OTP';
setOtpError: 'error.platform.vc-item.requestingLock:invocation[0]';
setOtpError:
| 'error.platform.vc-item.requestingLock:invocation[0]'
| 'error.platform.vc-item.requestingRevoke:invocation[0]';
setRevoke: 'done.invoke.vc-item.requestingRevoke:invocation[0]';
setTag: 'SAVE_TAG';
setTransactionId: 'LOCK_VC' | 'UNLOCK_VC';
setTransactionId:
| 'INPUT_OTP'
| 'REVOKE_VC'
| 'done.invoke.vc-item.requestingOtp:invocation[0]'
| 'error.platform.vc-item.requestingLock:invocation[0]'
| 'error.platform.vc-item.requestingRevoke:invocation[0]';
storeContext:
| 'CREDENTIAL_DOWNLOADED'
| 'done.invoke.vc-item.verifyingCredential:invocation[0]';
@@ -103,7 +129,8 @@ export interface Typegen0 {
checkStatus: 'STORE_RESPONSE';
downloadCredential: 'DOWNLOAD_READY';
requestLock: 'INPUT_OTP';
requestOtp: 'LOCK_VC' | 'UNLOCK_VC';
requestOtp: 'LOCK_VC';
requestRevoke: 'INPUT_OTP';
verifyCredential: '' | 'VERIFY';
};
'eventsCausingGuards': {
@@ -113,6 +140,7 @@ export interface Typegen0 {
'eventsCausingDelays': {};
'matchesStates':
| 'acceptingOtpInput'
| 'acceptingRevokeInput'
| 'checkingServerData'
| 'checkingServerData.checkingStatus'
| 'checkingServerData.downloadingCredential'
@@ -125,8 +153,11 @@ export interface Typegen0 {
| 'invalid.backend'
| 'invalid.otp'
| 'lockingVc'
| 'loggingRevoke'
| 'requestingLock'
| 'requestingOtp'
| 'requestingRevoke'
| 'revokingVc'
| 'storingTag'
| 'verifyingCredential'
| {

21
package-lock.json generated
View File

@@ -39,6 +39,7 @@
"expo-status-bar": "~1.2.0",
"expo-updates": "~0.11.6",
"i18next": "^21.6.16",
"mosip-mobileid-sdk": "^1.0.10",
"react": "17.0.1",
"react-i18next": "^11.16.6",
"react-native": "0.64.3",
@@ -17687,6 +17688,18 @@
"node": ">=10"
}
},
"node_modules/mosip-mobileid-sdk": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/mosip-mobileid-sdk/-/mosip-mobileid-sdk-1.0.10.tgz",
"integrity": "sha512-q4yhPUtI3iX1cMr/Di0zVeantaC3ONpaJdb7TWAmMKHIh6JG/yRkkmFlTeKrxOk3RQJ+AaVxXnaypXR2vtIlvw==",
"peerDependencies": {
"expo": "*",
"expo-camera": "*",
"expo-modules-core": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -41217,6 +41230,12 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"mosip-mobileid-sdk": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/mosip-mobileid-sdk/-/mosip-mobileid-sdk-1.0.10.tgz",
"integrity": "sha512-q4yhPUtI3iX1cMr/Di0zVeantaC3ONpaJdb7TWAmMKHIh6JG/yRkkmFlTeKrxOk3RQJ+AaVxXnaypXR2vtIlvw==",
"requires": {}
},
"move-concurrently": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@@ -49050,4 +49069,4 @@
}
}
}
}
}

View File

@@ -43,6 +43,7 @@
"expo-status-bar": "~1.2.0",
"expo-updates": "~0.11.6",
"i18next": "^21.6.16",
"mosip-mobileid-sdk": "^1.0.10",
"react": "17.0.1",
"react-i18next": "^11.16.6",
"react-native": "0.64.3",

View File

@@ -5,9 +5,9 @@ import {
} from '@react-navigation/bottom-tabs';
import { HomeScreen } from '../screens/Home/HomeScreen';
import { ProfileScreen } from '../screens/Profile/ProfileScreen';
import { ScanScreen } from '../screens/Scan/ScanScreen';
import { RootStackParamList } from './index';
import { RequestLayout } from '../screens/Request/RequestLayout';
import { ScanLayout } from '../screens/Scan/ScanLayout';
export const mainRoutes: TabScreen[] = [
{
@@ -17,7 +17,7 @@ export const mainRoutes: TabScreen[] = [
},
{
name: 'Scan',
component: ScanScreen,
component: ScanLayout,
icon: 'qr-code-scanner',
options: {
headerShown: false,

View File

@@ -1,3 +1,3 @@
{
"unlock": "Unlock with fingerprint"
"unlock": "Unlock with biometrics"
}

View File

@@ -11,6 +11,7 @@ import {
selectIsUnenrolled,
selectIsUnvailable,
} from '../machines/biometrics';
import { Platform } from 'react-native';
export function useBiometricScreen(props: RootRouteProps) {
const { appService } = useContext(GlobalContext);
@@ -21,11 +22,11 @@ export function useBiometricScreen(props: RootRouteProps) {
const [initAuthBio, updateInitAuthBio] = useState(true);
const [bioState, bioSend, bioService] = useMachine(biometricsMachine);
const isAuthorized: boolean = useSelector(authService, selectAuthorized);
const isAvailable: boolean = useSelector(bioService, selectIsAvailable);
const isUnavailable: boolean = useSelector(bioService, selectIsUnvailable);
const isSuccessBio: boolean = useSelector(bioService, selectIsSuccess);
const isUnenrolled: boolean = useSelector(bioService, selectIsUnenrolled);
const isAuthorized = useSelector(authService, selectAuthorized);
const isAvailable = useSelector(bioService, selectIsAvailable);
const isUnavailable = useSelector(bioService, selectIsUnvailable);
const isSuccessBio = useSelector(bioService, selectIsSuccess);
const isUnenrolled = useSelector(bioService, selectIsUnenrolled);
useEffect(() => {
console.log('bioState', bioState);
@@ -59,16 +60,20 @@ export function useBiometricScreen(props: RootRouteProps) {
}, [isAuthorized, isAvailable, isUnenrolled, isUnavailable, isSuccessBio]);
const checkBiometricsChange = () => {
RNFingerprintChange.hasFingerPrintChanged().then(
async (biometricsHasChanged: any) => {
//if new biometrics are added, re-enable Biometrics Authentication
if (biometricsHasChanged) {
setReEnabling(true);
} else {
bioSend({ type: 'AUTHENTICATE' });
if (Platform.OS === 'android') {
RNFingerprintChange.hasFingerPrintChanged().then(
async (biometricsHasChanged: any) => {
//if new biometrics are added, re-enable Biometrics Authentication
if (biometricsHasChanged) {
setReEnabling(true);
} else {
bioSend({ type: 'AUTHENTICATE' });
}
}
}
);
);
} else {
// TODO: solution for iOS
}
};
const useBiometrics = () => {

View File

@@ -38,7 +38,7 @@ export const HistoryTab: React.FC<HomeScreenTabProps> = (props) => {
}>
{controller.activities.map((activity) => (
<TextItem
key={activity.timestamp}
key={`${activity.timestamp}-${activity._vcKey}`}
label={createLabel(activity, i18n.language)}
text={`${activity.vcLabel} ${t(activity.action)}`}
/>

View File

@@ -53,6 +53,9 @@ export const HomeScreen: React.FC<HomeRouteProps> = (props) => {
isVisible={controller.isViewingVc}
onDismiss={controller.DISMISS_MODAL}
vcItemActor={controller.selectedVc}
onRevokeDelete={() => {
controller.REVOKE();
}}
/>
)}
</React.Fragment>

View File

@@ -12,6 +12,7 @@ import {
selectTabsLoaded,
selectViewingVc,
} from './HomeScreenMachine';
import { VcEvents } from '../../machines/vc';
export function useHomeScreen(props: HomeRouteProps) {
const { appService } = useContext(GlobalContext);
@@ -23,6 +24,7 @@ export function useHomeScreen(props: HomeRouteProps) {
);
const service = useInterpret(machine.current);
const settingsService = appService.children.get('settings');
const vcService = appService.children.get('vc');
useEffect(() => {
if (props.route.params?.activeTab != null) {
@@ -43,6 +45,10 @@ export function useHomeScreen(props: HomeRouteProps) {
SELECT_TAB,
DISMISS_MODAL: () => service.send(HomeScreenEvents.DISMISS_MODAL()),
REVOKE: () => {
vcService.send(VcEvents.REFRESH_MY_VCS());
service.send(HomeScreenEvents.DISMISS_MODAL());
},
};
function SELECT_TAB(index: number) {

View File

@@ -2,11 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
setSelectedVc: 'VIEW_VC';
spawnTabActors: 'xstate.init';
resetSelectedVc: 'DISMISS_MODAL';
};
'internalEvents': {
'xstate.after(100)#HomeScreen.tabs.init': {
type: 'xstate.after(100)#HomeScreen.tabs.init';
@@ -20,21 +15,26 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
resetSelectedVc: 'DISMISS_MODAL' | 'xstate.init';
setSelectedVc: 'VIEW_VC';
spawnTabActors: 'xstate.init';
};
'eventsCausingServices': {};
'eventsCausingGuards': {};
'eventsCausingDelays': {};
'matchesStates':
| 'tabs'
| 'tabs.init'
| 'tabs.myVcs'
| 'tabs.receivedVcs'
| 'tabs.history'
| 'modals'
| 'modals.none'
| 'modals.viewingVc'
| 'tabs'
| 'tabs.history'
| 'tabs.init'
| 'tabs.myVcs'
| 'tabs.receivedVcs'
| {
tabs?: 'init' | 'myVcs' | 'receivedVcs' | 'history';
modals?: 'none' | 'viewingVc';
tabs?: 'history' | 'init' | 'myVcs' | 'receivedVcs';
};
'tags': never;
}

View File

@@ -30,7 +30,7 @@ export const AddVcModal: React.FC<AddVcModalProps> = (props) => {
<MessageOverlay
isVisible={controller.isRequestingCredential}
title={t('requestingCredential')}
hasProgress
progress
/>
</React.Fragment>
);

View File

@@ -299,6 +299,9 @@ export const AddVcModalMachine =
},
requestCredential: async (context) => {
// force wait to fix issue with hanging overlay
await new Promise((resolve) => setTimeout(resolve, 1000));
const response = await request('POST', '/credentialshare/request', {
individualId: context.id,
individualIdType: context.idType,
@@ -312,7 +315,11 @@ export const AddVcModalMachine =
guards: {
isEmptyId: ({ id }) => !id || !id.length,
isWrongIdFormat: ({ id }) => !/^\d{10,16}$/.test(id),
isWrongIdFormat: ({ idType, id }) => {
const validIdType =
idType === 'UIN' ? id.length === 10 : id.length === 16;
return !(/^\d{10,16}$/.test(id) && validIdType);
},
isIdInvalid: (_context, event: unknown) =>
['IDA-MLC-009', 'RES-SER-29', 'IDA-MLC-018'].includes(

View File

@@ -3,8 +3,15 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'internalEvents': {
'xstate.after(100)#AddVcModal.acceptingIdInput.focusing': {
type: 'xstate.after(100)#AddVcModal.acceptingIdInput.focusing';
'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]': {
type: 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'done.invoke.AddVcModal.requestingCredential:invocation[0]': {
type: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
'error.platform.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]': {
type: 'error.platform.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
@@ -14,21 +21,14 @@ export interface Typegen0 {
type: 'error.platform.AddVcModal.requestingCredential:invocation[0]';
data: unknown;
};
'done.invoke.AddVcModal.requestingCredential:invocation[0]': {
type: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
'xstate.after(100)#AddVcModal.acceptingIdInput.focusing': {
type: 'xstate.after(100)#AddVcModal.acceptingIdInput.focusing';
};
'xstate.init': { type: 'xstate.init' };
'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]': {
type: 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
data: unknown;
__tip: 'See the XState TS docs to learn how to strongly type this.';
};
};
'invokeSrcNameMap': {
requestOtp: 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
requestCredential: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
requestOtp: 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
};
'missingImplementations': {
actions: never;
@@ -37,69 +37,69 @@ export interface Typegen0 {
delays: never;
};
'eventsCausingActions': {
forwardToParent: 'DISMISS';
setIdInputRef: 'READY';
setId: 'INPUT_ID';
setIdType: 'SELECT_ID_TYPE';
clearId: 'SELECT_ID_TYPE';
clearIdError: 'INPUT_ID';
setIdBackendError:
| 'error.platform.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]';
setOtp: 'INPUT_OTP';
resetIdInputRef: 'DISMISS';
setRequestId: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
setOtpError: 'error.platform.AddVcModal.requestingCredential:invocation[0]';
setTransactionId:
| 'xstate.init'
| 'DISMISS'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]';
clearOtp:
| 'xstate.init'
| 'DISMISS'
| 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]'
| 'done.invoke.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]';
| 'xstate.init';
focusInput:
| 'xstate.after(100)#AddVcModal.acceptingIdInput.focusing'
| 'INPUT_ID'
| 'SELECT_ID_TYPE'
| 'VALIDATE_INPUT'
| 'error.platform.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]'
| 'xstate.after(100)#AddVcModal.acceptingIdInput.focusing';
forwardToParent: 'DISMISS';
resetIdInputRef: 'DISMISS';
setId: 'INPUT_ID';
setIdBackendError:
| 'error.platform.AddVcModal.acceptingIdInput.requestingOtp:invocation[0]'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]';
setIdErrorEmpty: 'VALIDATE_INPUT';
setIdErrorWrongFormat: 'VALIDATE_INPUT';
setIdInputRef: 'READY';
setIdType: 'SELECT_ID_TYPE';
setOtp: 'INPUT_OTP';
setOtpError: 'error.platform.AddVcModal.requestingCredential:invocation[0]';
setRequestId: 'done.invoke.AddVcModal.requestingCredential:invocation[0]';
setTransactionId:
| 'DISMISS'
| 'error.platform.AddVcModal.requestingCredential:invocation[0]'
| 'xstate.init';
};
'eventsCausingServices': {
requestOtp: 'VALIDATE_INPUT';
requestCredential: 'INPUT_OTP';
requestOtp: 'VALIDATE_INPUT';
};
'eventsCausingGuards': {
isEmptyId: 'VALIDATE_INPUT';
isWrongIdFormat: 'VALIDATE_INPUT';
isIdInvalid: 'error.platform.AddVcModal.requestingCredential:invocation[0]';
isWrongIdFormat: 'VALIDATE_INPUT';
};
'eventsCausingDelays': {};
'matchesStates':
| 'acceptingIdInput'
| 'acceptingIdInput.rendering'
| 'acceptingIdInput.focusing'
| 'acceptingIdInput.idle'
| 'acceptingIdInput.invalid'
| 'acceptingIdInput.invalid.backend'
| 'acceptingIdInput.invalid.empty'
| 'acceptingIdInput.invalid.format'
| 'acceptingIdInput.invalid.backend'
| 'acceptingIdInput.rendering'
| 'acceptingIdInput.requestingOtp'
| 'acceptingOtpInput'
| 'requestingCredential'
| 'done'
| 'requestingCredential'
| {
acceptingIdInput?:
| 'rendering'
| 'focusing'
| 'idle'
| 'invalid'
| 'rendering'
| 'requestingOtp'
| { invalid?: 'empty' | 'format' | 'backend' };
| { invalid?: 'backend' | 'empty' | 'format' };
};
'tags': never;
}

View File

@@ -6,7 +6,7 @@ import { Modal } from '../../../components/ui/Modal';
import { Theme } from '../../../components/ui/styleUtils';
import { IdInputModalProps, useIdInputModal } from './IdInputModalController';
import { useTranslation } from 'react-i18next';
import { KeyboardAvoidingView, Platform } from 'react-native';
import { KeyboardAvoidingView, Platform, TextInput } from 'react-native';
import { TouchableOpacity } from 'react-native';
import { individualId } from '../../../shared/constants';
import { GET_INDIVIDUAL_ID } from '../../../shared/constants';
@@ -26,6 +26,9 @@ export const IdInputModal: React.FC<IdInputModalProps> = (props) => {
const inputLabel = t('enterId', { idType: controller.idType });
const setIdInputRef = (node: TextInput) =>
!controller.idInputRef && controller.READY(node);
return (
<Modal
onDismiss={dismissInput}
@@ -77,9 +80,7 @@ export const IdInputModal: React.FC<IdInputModalProps> = (props) => {
errorStyle={{ color: Theme.Colors.errorMessage }}
errorMessage={controller.idError}
onChangeText={controller.INPUT_ID}
ref={(node) =>
!controller.idInputRef && controller.READY(node)
}
ref={setIdInputRef}
/>
</Column>
</Row>

View File

@@ -0,0 +1,58 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet, View } from 'react-native';
import { Icon } from 'react-native-elements';
import { PinInput } from '../../../components/PinInput';
import { Column, Text } from '../../../components/ui';
import { ModalProps } from '../../../components/ui/Modal';
import { Colors } from '../../../components/ui/styleUtils';
const styles = StyleSheet.create({
modal: {
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
},
viewContainer: {
backgroundColor: Colors.White,
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
position: 'absolute',
top: 0,
zIndex: 9,
padding: 32,
},
close: {
position: 'absolute',
top: 32,
right: 0,
color: Colors.Orange,
},
});
export const OtpVerification: React.FC<OtpVerificationModalProps> = (props) => {
const { t } = useTranslation('OtpVerificationModal');
return (
<View style={styles.viewContainer}>
<Column fill padding="32" backgroundColor={Colors.White}>
<View style={styles.close}>
<Icon name="close" onPress={() => props.onDismiss()} />
</View>
<Icon name="lock" color={Colors.Orange} size={60} />
<Column fill align="space-between">
<Text align="center">{t('enterOtp')}</Text>
<Text align="center" color={Colors.Red} margin="16 0 0 0">
{props.error}
</Text>
<PinInput length={6} onDone={props.onInputDone} />
</Column>
<Column fill></Column>
</Column>
</View>
);
};
interface OtpVerificationModalProps extends ModalProps {
onInputDone: (otp: string) => void;
error?: string;
}

View File

@@ -41,9 +41,9 @@ export const MyVcsTab: React.FC<HomeScreenTabProps> = (props) => {
onRefresh={controller.REFRESH}
/>
}>
{controller.vcKeys.map((vcKey) => (
{controller.vcKeys.map((vcKey, index) => (
<VcItem
key={vcKey}
key={`${vcKey}-${index}`}
vcKey={vcKey}
margin="0 2 8 2"
onPress={controller.VIEW_VC}

View File

@@ -2,13 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
completeOnboarding: 'ADD_VC' | 'ONBOARDING_DONE';
sendVcAdded: 'STORE_RESPONSE';
getOnboardingStatus: 'xstate.init';
viewVcFromParent: 'VIEW_VC';
storeVcItem: 'done.invoke.AddVcModal';
};
'internalEvents': {
'done.invoke.AddVcModal': {
type: 'done.invoke.AddVcModal';
@@ -24,20 +17,27 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
completeOnboarding: 'ADD_VC' | 'ONBOARDING_DONE';
getOnboardingStatus: 'xstate.init';
sendVcAdded: 'STORE_RESPONSE';
storeVcItem: 'done.invoke.AddVcModal';
viewVcFromParent: 'VIEW_VC';
};
'eventsCausingServices': {};
'eventsCausingGuards': {
isOnboardingDone: 'STORE_RESPONSE';
};
'eventsCausingDelays': {};
'matchesStates':
| 'checkingOnboardingStatus'
| 'onboarding'
| 'idle'
| 'viewingVc'
| 'addingVc'
| 'addingVc.waitingForvcKey'
| 'addingVc.storing'
| 'addingVc.addVcSuccessful'
| { addingVc?: 'waitingForvcKey' | 'storing' | 'addVcSuccessful' };
| 'addingVc.storing'
| 'addingVc.waitingForvcKey'
| 'checkingOnboardingStatus'
| 'idle'
| 'onboarding'
| 'viewingVc'
| { addingVc?: 'addVcSuccessful' | 'storing' | 'waitingForvcKey' };
'tags': never;
}

View File

@@ -2,9 +2,6 @@
export interface Typegen0 {
'@@xstate/typegen': true;
'eventsCausingActions': {
viewVcFromParent: 'VIEW_VC';
};
'internalEvents': {
'xstate.init': { type: 'xstate.init' };
};
@@ -15,6 +12,9 @@ export interface Typegen0 {
guards: never;
delays: never;
};
'eventsCausingActions': {
viewVcFromParent: 'VIEW_VC';
};
'eventsCausingServices': {};
'eventsCausingGuards': {};
'eventsCausingDelays': {};

View File

@@ -1,9 +1,17 @@
{
"cancel": "Cancel",
"lock": "Lock",
"unlock": "Unlock",
"rename": "Rename",
"delete": "Delete",
"revoke": "Revoke",
"revoking": "Your wallet contains a credential with VID {{vid}}. Revoking this will automatically remove the same from the wallet. Are you sure you want to proceed?",
"requestingOtp": "Requesting OTP...",
"editTag": "Edit Tag"
"editTag": "Rename",
"redirecting": "Redirecting...",
"success": {
"unlocked": "{{vcLabel}} successfully unlocked",
"locked": "{{vcLabel}} successfully locked",
"revoked": "VID {{vid}} has been revoked. Any credential containing the same will be removed automatically from the wallet"
}
}

View File

@@ -1,21 +1,43 @@
import React from 'react';
import { Icon } from 'react-native-elements';
import { DropdownIcon } from '../../components/DropdownIcon';
import { TextEditOverlay } from '../../components/TextEditOverlay';
import { Column } from '../../components/ui';
import { Modal } from '../../components/ui/Modal';
import { Theme } from '../../components/ui/styleUtils';
import { MessageOverlay } from '../../components/MessageOverlay';
import { ToastItem } from '../../components/ui/ToastItem';
import { Passcode } from '../../components/Passcode';
import { OtpVerificationModal } from './MyVcs/OtpVerificationModal';
import { RevokeConfirmModal } from '../../components/RevokeConfirm';
import { OIDcAuthenticationModal } from '../../components/OIDcAuth';
import { useViewVcModal, ViewVcModalProps } from './ViewVcModalController';
import { useTranslation } from 'react-i18next';
import { VcDetails } from '../../components/VcDetails';
import { OtpVerification } from './MyVcs/OtpVerification';
export const ViewVcModal: React.FC<ViewVcModalProps> = (props) => {
const { t } = useTranslation('ViewVcModal');
const controller = useViewVcModal(props);
const DATA = [
{
idType: 'UIN',
label: controller.vc.locked ? 'Unlock' : 'Lock',
icon: 'lock-outline',
onPress: () => controller.lockVc(),
},
{
idType: 'VID',
label: t('revoke'),
icon: 'close-circle-outline',
onPress: () => controller.CONFIRM_REVOKE_VC(),
},
{
label: t('editTag'),
icon: 'pencil',
onPress: () => controller.EDIT_TAG(),
},
];
return (
<Modal
isVisible={props.isVisible}
@@ -23,10 +45,10 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = (props) => {
headerTitle={controller.vc.tag || controller.vc.id}
headerElevation={2}
headerRight={
<Icon
name="edit"
onPress={controller.EDIT_TAG}
color={Theme.Colors.Icon}
<DropdownIcon
icon="dots-vertical"
idType={controller.vc.idType}
items={DATA}
/>
}>
<Column scroll>
@@ -34,38 +56,50 @@ export const ViewVcModal: React.FC<ViewVcModalProps> = (props) => {
<VcDetails vc={controller.vc} />
</Column>
</Column>
{controller.isEditingTag && (
<TextEditOverlay
isVisible={controller.isEditingTag}
label={t('editTag')}
value={controller.vc.tag}
onDismiss={controller.DISMISS}
onSave={controller.SAVE_TAG}
/>
)}
<TextEditOverlay
isVisible={controller.isEditingTag}
label={t('editTag')}
value={controller.vc.tag}
onDismiss={controller.DISMISS}
onSave={controller.SAVE_TAG}
/>
{controller.isAcceptingRevokeInput && (
<OIDcAuthenticationModal
isVisible={controller.isAcceptingRevokeInput}
onDismiss={controller.DISMISS}
onVerify={() => {
controller.revokeVc('111111');
}}
error={controller.otpError}
/>
)}
<OtpVerificationModal
isVisible={controller.isAcceptingOtpInput}
onDismiss={controller.DISMISS}
onInputDone={controller.inputOtp}
error={controller.otpError}
/>
{controller.isAcceptingOtpInput && (
<OtpVerification
isVisible={controller.isAcceptingOtpInput}
onDismiss={controller.DISMISS}
onInputDone={controller.inputOtp}
error={controller.otpError}
/>
)}
<MessageOverlay
isVisible={controller.isRequestingOtp}
title={t('requestingOtp')}
hasProgress
progress
/>
{controller.reAuthenticating !== '' &&
controller.reAuthenticating == 'passcode' && (
<Passcode
onSuccess={() => controller.onSuccess()}
onError={(value) => controller.onError(value)}
storedPasscode={controller.storedPasscode}
onDismiss={() => controller.setReAuthenticating('')}
error={controller.error}
/>
)}
{controller.isRevoking && (
<RevokeConfirmModal
id={controller.vc.id}
onCancel={() => controller.setRevoking(false)}
onRevoke={controller.REVOKE_VC}
/>
)}
{controller.toastVisible && <ToastItem message={controller.message} />}
</Modal>
);

View File

@@ -1,55 +1,55 @@
import { useMachine, useSelector } from '@xstate/react';
import { useContext, useEffect, useState } from 'react';
import { ActorRefFrom } from 'xstate';
import { useTranslation } from 'react-i18next';
import NetInfo from '@react-native-community/netinfo';
import { ModalProps } from '../../components/ui/Modal';
import { GlobalContext } from '../../shared/GlobalContext';
import {
selectOtpError,
selectIsAcceptingOtpInput,
selectIsAcceptingRevokeInput,
selectIsEditingTag,
selectIsLockingVc,
selectIsRequestingOtp,
selectIsRevokingVc,
selectIsLoggingRevoke,
selectVc,
VcItemEvents,
vcItemMachine,
} from '../../machines/vcItem';
import { selectPasscode } from '../../machines/auth';
import {
biometricsMachine,
selectIsAvailable,
selectIsSuccess,
} from '../../machines/biometrics';
import { selectBiometricUnlockEnabled } from '../../machines/settings';
import { biometricsMachine, selectIsSuccess } from '../../machines/biometrics';
import { selectVcLabel } from '../../machines/settings';
export function useViewVcModal({ vcItemActor, isVisible }: ViewVcModalProps) {
export function useViewVcModal({
vcItemActor,
isVisible,
onRevokeDelete,
}: ViewVcModalProps) {
const { t } = useTranslation('ViewVcModal');
const [toastVisible, setToastVisible] = useState(false);
const [message, setMessage] = useState('');
const [reAuthenticating, setReAuthenticating] = useState('');
const [isRevoking, setRevoking] = useState(false);
const [error, setError] = useState('');
const { appService } = useContext(GlobalContext);
const authService = appService.children.get('auth');
const settingsService = appService.children.get('settings');
const [, bioSend, bioService] = useMachine(biometricsMachine);
const isBiometricUnlockEnabled = useSelector(
settingsService,
selectBiometricUnlockEnabled
);
const isAvailable = useSelector(bioService, selectIsAvailable);
const isSuccessBio = useSelector(bioService, selectIsSuccess);
const isLockingVc = useSelector(vcItemActor, selectIsLockingVc);
const vc = useSelector(vcItemActor, selectVc);
const isSuccessBio = useSelector(bioService, selectIsSuccess);
const vcLabel = useSelector(settingsService, selectVcLabel);
const isLockingVc = useSelector(vcItemActor, selectIsLockingVc);
const isRevokingVc = useSelector(vcItemActor, selectIsRevokingVc);
const isLoggingRevoke = useSelector(vcItemActor, selectIsLoggingRevoke);
const vc = useSelector(vcItemActor, selectVc);
const otError = useSelector(vcItemActor, selectOtpError);
const onSuccess = () => {
bioSend({ type: 'SET_IS_AVAILABLE', data: true });
setError('');
setReAuthenticating('');
if (vc.locked) {
vcItemActor.send(VcItemEvents.UNLOCK_VC());
} else {
vcItemActor.send(VcItemEvents.LOCK_VC());
}
vcItemActor.send(VcItemEvents.LOCK_VC());
};
const onError = (value: string) => {
@@ -65,21 +65,48 @@ export function useViewVcModal({ vcItemActor, isVisible }: ViewVcModalProps) {
}, 3000);
};
const netInfoFetch = (otp: string) => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
vcItemActor.send(VcItemEvents.INPUT_OTP(otp));
} else {
vcItemActor.send(VcItemEvents.DISMISS());
showToast('Request network failed');
}
});
};
useEffect(() => {
if (isLockingVc) {
showToast(
vc.locked ? 'ID successfully locked' : 'ID successfully unlocked'
vc.locked
? t('success.locked', { vcLabel: vcLabel.singular })
: t('success.unlocked', { vcLabel: vcLabel.singular })
);
}
if (isRevokingVc) {
showToast(t('success.revoked', { vid: vc.id }));
}
if (isLoggingRevoke) {
vcItemActor.send(VcItemEvents.DISMISS());
onRevokeDelete();
}
if (isSuccessBio && reAuthenticating != '') {
onSuccess();
}
}, [reAuthenticating, isLockingVc, isSuccessBio, otError]);
}, [
reAuthenticating,
isLockingVc,
isSuccessBio,
otError,
isRevokingVc,
isLoggingRevoke,
vc,
]);
useEffect(() => {
vcItemActor.send(VcItemEvents.REFRESH());
}, [isVisible]);
return {
error,
message,
@@ -87,48 +114,48 @@ export function useViewVcModal({ vcItemActor, isVisible }: ViewVcModalProps) {
vc,
otpError: useSelector(vcItemActor, selectOtpError),
reAuthenticating,
isRevoking,
isEditingTag: useSelector(vcItemActor, selectIsEditingTag),
isLockingVc,
isAcceptingOtpInput: useSelector(vcItemActor, selectIsAcceptingOtpInput),
isAcceptingRevokeInput: useSelector(
vcItemActor,
selectIsAcceptingRevokeInput
),
isRequestingOtp: useSelector(vcItemActor, selectIsRequestingOtp),
storedPasscode: useSelector(authService, selectPasscode),
CONFIRM_REVOKE_VC: () => {
setRevoking(true);
},
REVOKE_VC: () => {
vcItemActor.send(VcItemEvents.REVOKE_VC());
setRevoking(false);
},
setReAuthenticating,
setRevoking,
onError,
lockVc: () => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
if (isAvailable && isBiometricUnlockEnabled) {
setReAuthenticating('biometrics');
bioSend({ type: 'AUTHENTICATE' });
} else {
setReAuthenticating('passcode');
}
} else {
showToast('Request network failed');
}
});
vcItemActor.send(VcItemEvents.LOCK_VC());
},
inputOtp: (otp: string) => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
vcItemActor.send(VcItemEvents.INPUT_OTP(otp));
} else {
vcItemActor.send(VcItemEvents.DISMISS());
showToast('Request network failed');
}
});
netInfoFetch(otp);
},
revokeVc: (otp: string) => {
netInfoFetch(otp);
},
onSuccess,
EDIT_TAG: () => vcItemActor.send(VcItemEvents.EDIT_TAG()),
SAVE_TAG: (tag: string) => vcItemActor.send(VcItemEvents.SAVE_TAG(tag)),
DISMISS: () => vcItemActor.send(VcItemEvents.DISMISS()),
LOCK_VC: () => vcItemActor.send(VcItemEvents.LOCK_VC()),
UNLOCK_VC: () => vcItemActor.send(VcItemEvents.UNLOCK_VC()),
INPUT_OTP: (otp: string) => vcItemActor.send(VcItemEvents.INPUT_OTP(otp)),
};
}
export interface ViewVcModalProps extends ModalProps {
vcItemActor: ActorRefFrom<typeof vcItemMachine>;
onDismiss: () => void;
onRevokeDelete: () => void;
}

View File

@@ -1,6 +1,12 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, Image, StyleSheet, View } from 'react-native';
import {
Dimensions,
Image,
SafeAreaView,
StyleSheet,
View,
} from 'react-native';
import { Divider, Icon, ListItem, Overlay } from 'react-native-elements';
import Markdown from 'react-native-simple-markdown';
import { Button, Text, Row } from '../../components/ui';
@@ -64,25 +70,27 @@ export const Credits: React.FC<CreditsProps> = (props) => {
overlayStyle={{ padding: 24 }}
isVisible={isViewing}
onBackdropPress={() => setIsViewing(false)}>
<View style={styles.view}>
<Row align="center" crossAlign="center" margin="0 0 10 0">
<View style={styles.buttonContainer}>
<Button
type="clear"
icon={<Icon name="chevron-left" color={Theme.Colors.Icon} />}
title=""
onPress={() => setIsViewing(false)}
/>
<SafeAreaView>
<View style={styles.view}>
<Row align="center" crossAlign="center" margin="0 0 10 0">
<View style={styles.buttonContainer}>
<Button
type="clear"
icon={<Icon name="chevron-left" color={Theme.Colors.Icon} />}
title=""
onPress={() => setIsViewing(false)}
/>
</View>
<Text size="small">{t('header')}</Text>
</Row>
<Divider />
<View style={styles.markdownView}>
<Markdown rules={rules} styles={markdownStyles}>
{creditsContent}
</Markdown>
</View>
<Text size="small">{t('header')}</Text>
</Row>
<Divider />
<View style={styles.markdownView}>
<Markdown rules={rules} styles={markdownStyles}>
{creditsContent}
</Markdown>
</View>
</View>
</SafeAreaView>
</Overlay>
</ListItem>
);

View File

@@ -5,5 +5,11 @@
"bioUnlock": "Biometric unlock",
"authFactorUnlock": "Unlock auth factor",
"credits": "Credits and legal notices",
"logout": "Log-out"
"logout": "Log-out",
"revokeLabel": "Revoke VID",
"revokeHeader": "REVOKE VID",
"revokingVids": "You are about to revoke ({{count}}) VIDs.",
"revokingVidsAfter": "This means you will no longer be able to use or view any of the IDs linked to those VID(s). \nAre you sure you want to proceed?",
"empty": "Empty",
"revokeSuccessful": "VID successfully revoked"
}

View File

@@ -8,6 +8,7 @@ import { MainRouteProps } from '../../routes/main';
import { EditableListItem } from '../../components/EditableListItem';
import { MessageOverlay } from '../../components/MessageOverlay';
import { Credits } from './Credits';
import { Revoke } from './Revoke';
import { useProfileScreen } from './ProfileScreenController';
import { useTranslation } from 'react-i18next';
import { LanguageSelector } from '../../components/LanguageSelector';
@@ -58,6 +59,7 @@ export const ProfileScreen: React.FC<MainRouteProps> = (props) => {
onEdit={controller.UPDATE_VC_LABEL}
/>
<LanguageSetting />
<Revoke label={t('revokeLabel')} />
<ListItem bottomDivider disabled={!controller.canUseBiometrics}>
<ListItem.Content>
<ListItem.Title>

189
screens/Profile/Revoke.tsx Normal file
View File

@@ -0,0 +1,189 @@
import React from 'react';
import {
Dimensions,
RefreshControl,
SafeAreaView,
StyleSheet,
View,
} from 'react-native';
import { Divider, Icon, ListItem, Overlay } from 'react-native-elements';
import { Button, Column, Centered, Row, Text } from '../../components/ui';
import { VidItem } from '../../components/VidItem';
import { Colors } from '../../components/ui/styleUtils';
import { ToastItem } from '../../components/ui/ToastItem';
import { OIDcAuthenticationOverlay } from '../../components/OIDcAuthModal';
import { useTranslation } from 'react-i18next';
import { useRevoke } from './RevokeController';
export const Revoke: React.FC<RevokeScreenProps> = (props) => {
const controller = useRevoke();
const { t } = useTranslation('ProfileScreen');
const styles = StyleSheet.create({
buttonContainer: {
position: 'absolute',
left: 0,
right: 'auto',
},
view: {
flex: 1,
width: Dimensions.get('screen').width,
},
revokeView: { padding: 20 },
flexRow: { flexDirection: 'row', margin: 0, padding: 0 },
rowStyle: { flexDirection: 'column', justifyContent: 'space-between' },
viewContainer: {
backgroundColor: 'rgba(0,0,0,.6)',
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
position: 'absolute',
top: 0,
zIndex: 999,
},
boxContainer: {
backgroundColor: Colors.White,
padding: 24,
elevation: 6,
borderRadius: 4,
},
});
return (
<ListItem bottomDivider onPress={() => controller.setAuthenticating(true)}>
<ListItem.Content>
<ListItem.Title>
<Text>{props.label}</Text>
</ListItem.Title>
</ListItem.Content>
<Overlay
overlayStyle={{ padding: 0 }}
isVisible={controller.isViewing}
onBackdropPress={() => controller.setIsViewing(false)}>
<SafeAreaView>
{controller.toastVisible && (
<ToastItem message={controller.message} />
)}
<View style={styles.view}>
<Row align="center" crossAlign="center" margin="0 0 10 0">
<View style={styles.buttonContainer}>
<Button
type="clear"
icon={<Icon name="chevron-left" color={Colors.Orange} />}
title=""
onPress={() => controller.setIsViewing(false)}
/>
</View>
<Text size="small">{t('revokeHeader')}</Text>
</Row>
<Divider />
<Row style={styles.rowStyle} fill>
<View style={styles.revokeView}>
{controller.vidKeys.length > 0 && (
<Column
scroll
refreshControl={
<RefreshControl
refreshing={controller.isRefreshingVcs}
onRefresh={controller.REFRESH}
/>
}>
{controller.vidKeys.map((vcKey, index) => (
<VidItem
key={`${vcKey}-${index}`}
vcKey={vcKey}
margin="0 2 8 2"
onPress={controller.selectVcItem(index, vcKey)}
selectable
selected={controller.selectedVidKeys.includes(vcKey)}
/>
))}
</Column>
)}
{controller.vidKeys.length === 0 && (
<React.Fragment>
<Centered fill>
<Text weight="semibold" margin="0 0 8 0">
{t('empty')}
</Text>
</Centered>
</React.Fragment>
)}
</View>
<Column margin="0 20">
<Button
disabled={controller.selectedVidKeys.length === 0}
title={t('revokeHeader')}
onPress={controller.CONFIRM_REVOKE_VC}
/>
</Column>
</Row>
</View>
</SafeAreaView>
{controller.isRevoking && (
<View style={styles.viewContainer}>
<Centered fill>
<Column
width={Dimensions.get('screen').width * 0.8}
style={styles.boxContainer}>
<Text weight="semibold" margin="0 0 12 0">
{t('revokeLabel')}
</Text>
<Text margin="0 0 12 0">
{t('revokingVids', {
count: controller.selectedVidKeys.length,
})}
</Text>
{controller.selectedVidKeys.map((vcKey, index) => (
<View style={styles.flexRow} key={index}>
<Text margin="0 8" weight="bold">
{'\u2022'}
</Text>
<Text margin="0 0 0 0" weight="bold">
{vcKey.split(':')[2]}
</Text>
</View>
))}
<Text margin="12 0">{t('revokingVidsAfter')}</Text>
<Row>
<Button
fill
type="clear"
title={t('cancel')}
onPress={() => controller.setRevoking(false)}
/>
<Button
fill
title={t('revokeLabel')}
onPress={controller.REVOKE_VC}
/>
</Row>
</Column>
</Centered>
</View>
)}
</Overlay>
<OIDcAuthenticationOverlay
isVisible={controller.isAuthenticating}
onDismiss={() => controller.setAuthenticating(false)}
onVerify={() => {
controller.setAuthenticating(false);
controller.setIsViewing(true);
}}
/>
<OIDcAuthenticationOverlay
isVisible={controller.isAcceptingOtpInput}
onDismiss={controller.DISMISS}
onVerify={() => {
controller.setIsViewing(true);
controller.revokeVc('111111');
}}
/>
</ListItem>
);
};
interface RevokeScreenProps {
label: string;
}

View File

@@ -0,0 +1,128 @@
import { useSelector } from '@xstate/react';
import { useContext, useEffect, useState } from 'react';
import NetInfo from '@react-native-community/netinfo';
import { GlobalContext } from '../../shared/GlobalContext';
import {
selectIsRefreshingMyVcs,
selectMyVcs,
VcEvents,
} from '../../machines/vc';
import { vcItemMachine } from '../../machines/vcItem';
import { useTranslation } from 'react-i18next';
import {
RevokeVidsEvents,
selectIsAcceptingOtpInput,
selectIsRevokingVc,
selectIsLoggingRevoke,
} from '../../machines/revoke';
import { ActorRefFrom } from 'xstate';
export function useRevoke() {
const { t } = useTranslation('ProfileScreen');
const { appService } = useContext(GlobalContext);
const vcService = appService.children.get('vc');
const revokeService = appService.children.get('RevokeVids');
const vcKeys = useSelector(vcService, selectMyVcs);
const isRevokingVc = useSelector(revokeService, selectIsRevokingVc);
const isLoggingRevoke = useSelector(revokeService, selectIsLoggingRevoke);
const isAcceptingOtpInput = useSelector(
revokeService,
selectIsAcceptingOtpInput
);
const [isRevoking, setRevoking] = useState(false);
const [isAuthenticating, setAuthenticating] = useState(false);
const [isViewing, setIsViewing] = useState(false);
const [toastVisible, setToastVisible] = useState(false);
const [message, setMessage] = useState('');
const [selectedIndex, setSelectedIndex] = useState<number>(null);
const [selectedVidKeys, setSelectedVidKeys] = useState<string[]>([]);
const vidKeys = vcKeys.filter((vc) => {
const vcKey = vc.split(':');
return vcKey[1] === 'VID';
});
const selectVcItem = (index: number, vcKey: string) => {
return () => {
setSelectedIndex(index);
if (selectedVidKeys.includes(vcKey)) {
setSelectedVidKeys(selectedVidKeys.filter((item) => item !== vcKey));
} else {
setSelectedVidKeys((prevArray) => [...prevArray, vcKey]);
}
};
};
const showToast = (message: string) => {
setToastVisible(true);
setMessage(message);
setTimeout(() => {
setToastVisible(false);
setMessage('');
}, 3000);
};
useEffect(() => {
if (isRevokingVc) {
setSelectedVidKeys([]);
showToast(t('revokeSuccessful'));
}
if (isLoggingRevoke) {
revokeService.send(RevokeVidsEvents.DISMISS());
vcService.send(VcEvents.REFRESH_MY_VCS());
}
}, [isRevokingVc, isLoggingRevoke]);
return {
error: '',
isAcceptingOtpInput,
isAuthenticating,
isRefreshingVcs: useSelector(vcService, selectIsRefreshingMyVcs),
isRevoking,
isViewing,
message,
selectedIndex,
selectedVidKeys,
toastVisible,
vidKeys: vidKeys.filter(
(vcKey, index, vid) => vid.indexOf(vcKey) === index
),
CONFIRM_REVOKE_VC: () => {
setRevoking(true);
},
DISMISS: () => {
revokeService.send(RevokeVidsEvents.DISMISS());
},
INPUT_OTP: (otp: string) =>
revokeService.send(RevokeVidsEvents.INPUT_OTP(otp)),
REFRESH: () => vcService.send(VcEvents.REFRESH_MY_VCS()),
REVOKE_VC: () => {
revokeService.send(RevokeVidsEvents.REVOKE_VCS(selectedVidKeys));
setRevoking(false);
//since nested modals/overlays don't work in ios, we need to toggle revoke screen
setIsViewing(false);
},
revokeVc: (otp: string) => {
NetInfo.fetch().then((state) => {
if (state.isConnected) {
revokeService.send(RevokeVidsEvents.INPUT_OTP(otp));
} else {
revokeService.send(RevokeVidsEvents.DISMISS());
showToast('Request network failed');
}
});
},
setAuthenticating,
selectVcItem,
setIsViewing,
setRevoking,
};
}
export interface RevokeProps {
service: ActorRefFrom<typeof vcItemMachine>;
}

View File

@@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
import { RequestScreen } from './RequestScreen';
import { useRequestLayout } from './RequestLayoutController';
import { MessageOverlay } from '../../components/MessageOverlay';
import { Message } from '../../components/Message';
import { ReceiveVcScreen } from './ReceiveVcScreen';
import { LanguageSelector } from '../../components/LanguageSelector';
import { Theme } from '../../components/ui/styleUtils';
@@ -50,32 +50,35 @@ export const RequestLayout: React.FC = () => {
/>
</RequestStack.Navigator>
<MessageOverlay
isVisible={controller.isAccepted}
title={t('status.accepted.title')}
message={t('status.accepted.message', {
vcLabel: controller.vcLabel.singular,
sender: controller.senderInfo.deviceName,
})}
onBackdropPress={controller.DISMISS}
/>
{controller.isAccepted && (
<Message
title={t('status.accepted.title')}
message={t('status.accepted.message', {
vcLabel: controller.vcLabel.singular,
sender: controller.senderInfo.deviceName,
})}
onBackdropPress={controller.DISMISS}
/>
)}
<MessageOverlay
isVisible={controller.isRejected}
title={t('status.rejected.title')}
message={t('status.rejected.message', {
vcLabel: controller.vcLabel.singular,
sender: controller.senderInfo.deviceName,
})}
onBackdropPress={controller.DISMISS}
/>
{controller.isRejected && (
<Message
title={t('status.rejected.title')}
message={t('status.rejected.message', {
vcLabel: controller.vcLabel.singular,
sender: controller.senderInfo.deviceName,
})}
onBackdropPress={controller.DISMISS}
/>
)}
<MessageOverlay
isVisible={controller.isDisconnected}
title={t('status.disconnected.title')}
message={t('status.disconnected.message')}
onBackdropPress={controller.DISMISS}
/>
{controller.isDisconnected && (
<Message
title={t('status.disconnected.title')}
message={t('status.disconnected.message')}
onBackdropPress={controller.DISMISS}
/>
)}
</React.Fragment>
);
};

View File

@@ -17,8 +17,14 @@
"message": "The connection was interrupted. Please try again."
},
"waitingConnection": "Waiting for connection...",
"exchangingDeviceInfo": "Exchanging device info...",
"connected": "Connected to device. Waiting for {{vcLabel}}..."
"exchangingDeviceInfo": {
"message": "Exchanging device info...",
"timeoutHint": "It's taking too long to exchange device info..."
},
"connected": {
"message": "Connected to device. Waiting for {{vcLabel}}...",
"timeoutHint": "No VC data received yet. Is sending device still connected?"
}
},
"online": "Online",
"offline": "Offline",

View File

@@ -3,7 +3,7 @@ import QRCode from 'react-native-qrcode-svg';
import { Centered, Button, Row, Column, Text } from '../../components/ui';
import { Theme } from '../../components/ui/styleUtils';
import { useRequestScreen } from './RequestScreenController';
import { useTranslation } from 'react-i18next';
import { TFunction, useTranslation } from 'react-i18next';
import { Switch } from 'react-native-elements';
import { Platform } from 'react-native';
@@ -16,24 +16,66 @@ export const RequestScreen: React.FC = () => {
fill
padding="24"
backgroundColor={Theme.Colors.lightGreyBackgroundColor}>
<Column>
{controller.isBluetoothDenied ? (
<React.Fragment>
<Text color={Theme.Colors.errorMessage} align="center">
{t('bluetoothDenied', { vcLabel: controller.vcLabel.singular })}
</Text>
<Button
margin={[32, 0, 0, 0]}
title={t('gotoSettings')}
onPress={controller.GOTO_SETTINGS}
/>
</React.Fragment>
) : (
<Text align="center">
{t('showQrCode', { vcLabel: controller.vcLabel.singular })}
{controller.isBluetoothDenied && (
<BluetoothPrompt t={t} controller={controller} />
)}
{!controller.isCheckingBluetoothService &&
!controller.isBluetoothDenied ? (
<Column align="flex-end" fill>
{controller.isWaitingForConnection && (
<SharingCode t={t} controller={controller} />
)}
<StatusMessage t={t} controller={controller} />
</Column>
) : null}
</Column>
);
};
const BluetoothPrompt: React.FC<RequestScreenProps> = ({ t, controller }) => {
return (
<Centered fill>
<Text color={Theme.Colors.errorMessage} align="center">
{t('bluetoothDenied', { vcLabel: controller.vcLabel.singular })}
</Text>
<Button
margin={[32, 0, 0, 0]}
title={t('gotoSettings')}
onPress={controller.GOTO_SETTINGS}
/>
</Centered>
);
};
const StatusMessage: React.FC<RequestScreenProps> = ({ t, controller }) => {
return (
controller.statusMessage !== '' && (
<Column elevation={1} padding="16 24">
<Text>{controller.statusMessage}</Text>
{controller.statusHint !== '' && (
<Text size="small" color={Theme.Colors.textLabel}>
{controller.statusHint}
</Text>
)}
{controller.isStatusCancellable && (
<Button
margin={[8, 0, 0, 0]}
title={t('cancel', { ns: 'common' })}
onPress={controller.CANCEL}
/>
)}
</Column>
)
);
};
const SharingCode: React.FC<RequestScreenProps> = ({ t, controller }) => {
return (
<React.Fragment>
<Text align="center">
{t('showQrCode', { vcLabel: controller.vcLabel.singular })}
</Text>
<Centered fill>
{controller.connectionParams !== '' ? (
@@ -54,12 +96,11 @@ export const RequestScreen: React.FC = () => {
/>
<Text margin={[0, 0, 0, 16]}>Online</Text>
</Row>
{controller.statusMessage !== '' && (
<Column elevation={1} padding="16 24">
<Text>{controller.statusMessage}</Text>
</Column>
)}
</Column>
</React.Fragment>
);
};
interface RequestScreenProps {
t: TFunction;
controller: ReturnType<typeof useRequestScreen>;
}

View File

@@ -11,6 +11,9 @@ import {
selectIsExchangingDeviceInfo,
selectIsWaitingForVc,
selectSharingProtocol,
selectIsExchangingDeviceInfoTimeout,
selectIsWaitingForVcTimeout,
selectIsCheckingBluetoothService,
} from '../../machines/request';
import { selectVcLabel } from '../../machines/settings';
import { GlobalContext } from '../../shared/GlobalContext';
@@ -38,15 +41,37 @@ export function useRequestScreen() {
requestService,
selectIsExchangingDeviceInfo
);
const isExchangingDeviceInfoTimeout = useSelector(
requestService,
selectIsExchangingDeviceInfoTimeout
);
const isWaitingForVc = useSelector(requestService, selectIsWaitingForVc);
const isWaitingForVcTimeout = useSelector(
requestService,
selectIsWaitingForVcTimeout
);
let statusMessage = '';
let statusHint = '';
let isStatusCancellable = false;
if (isWaitingForConnection) {
statusMessage = t('status.waitingConnection');
} else if (isExchangingDeviceInfo) {
statusMessage = t('status.exchangingDeviceInfo');
statusMessage = t('status.exchangingDeviceInfo.message');
} else if (isExchangingDeviceInfoTimeout) {
statusMessage = t('status.exchangingDeviceInfo.message');
statusHint = t('status.exchangingDeviceInfo.timeoutHint');
isStatusCancellable = true;
} else if (isWaitingForVc) {
statusMessage = t('status.connected', { vcLabel: vcLabel.singular });
statusMessage = t('status.connected.message', {
vcLabel: vcLabel.singular,
});
} else if (isWaitingForVcTimeout) {
statusMessage = t('status.connected.message', {
vcLabel: vcLabel.singular,
});
statusHint = t('status.connected.timeoutHint');
isStatusCancellable = true;
}
useEffect(() => {
@@ -60,17 +85,24 @@ export function useRequestScreen() {
return {
vcLabel,
statusMessage,
statusHint,
sharingProtocol: useSelector(requestService, selectSharingProtocol),
isWaitingForConnection,
isExchangingDeviceInfo,
isStatusCancellable,
isWaitingForVc,
isBluetoothDenied,
isCheckingBluetoothService: useSelector(
requestService,
selectIsCheckingBluetoothService
),
connectionParams: useSelector(requestService, selectConnectionParams),
senderInfo: useSelector(requestService, selectSenderInfo),
isReviewing: useSelector(requestService, selectIsReviewing),
CANCEL: () => requestService.send(RequestEvents.CANCEL()),
DISMISS: () => requestService.send(RequestEvents.DISMISS()),
ACCEPT: () => requestService.send(RequestEvents.ACCEPT()),
REJECT: () => requestService.send(RequestEvents.REJECT()),

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { Icon } from 'react-native-elements';
import { Colors } from '../../components/ui/styleUtils';
import { SendVcScreen } from './SendVcScreen';
import { MessageOverlay } from '../../components/MessageOverlay';
import { useScanLayout } from './ScanLayoutController';
import { LanguageSelector } from '../../components/LanguageSelector';
import { ScanScreen } from './ScanScreen';
const ScanStack = createNativeStackNavigator();
export const ScanLayout: React.FC = () => {
const { t } = useTranslation('ScanScreen');
const controller = useScanLayout();
return (
<React.Fragment>
<ScanStack.Navigator
initialRouteName="ScanScreen"
screenOptions={{
headerTitleAlign: 'center',
headerRight: () => (
<LanguageSelector
triggerComponent={<Icon name="language" color={Colors.Orange} />}
/>
),
}}>
{!controller.isDone && (
<ScanStack.Screen
name="SendVcScreen"
component={SendVcScreen}
options={{
title: t('sharingVc', {
vcLabel: controller.vcLabel.singular,
}),
}}
/>
)}
<ScanStack.Screen
name="ScanScreen"
component={ScanScreen}
options={{
title: t('scan').toUpperCase(),
}}
/>
</ScanStack.Navigator>
<MessageOverlay
isVisible={controller.statusOverlay != null}
message={controller.statusOverlay?.message}
hint={controller.statusOverlay?.hint}
onCancel={controller.statusOverlay?.onCancel}
progress={!controller.isInvalid}
onBackdropPress={controller.DISMISS_INVALID}
/>
</React.Fragment>
);
};

View File

@@ -0,0 +1,136 @@
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { useSelector } from '@xstate/react';
import { useContext, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { MessageOverlayProps } from '../../components/MessageOverlay';
import {
ScanEvents,
selectIsInvalid,
selectIsLocationDisabled,
selectIsLocationDenied,
selectIsConnecting,
selectIsExchangingDeviceInfo,
selectIsConnectingTimeout,
selectIsExchangingDeviceInfoTimeout,
selectIsDone,
selectIsReviewing,
selectIsScanning,
} from '../../machines/scan';
import { selectVcLabel } from '../../machines/settings';
import { MainBottomTabParamList } from '../../routes/main';
import { GlobalContext } from '../../shared/GlobalContext';
type ScanStackParamList = {
ScanScreen: undefined;
SendVcScreen: undefined;
};
type ScanLayoutNavigation = NavigationProp<
ScanStackParamList & MainBottomTabParamList
>;
export function useScanLayout() {
const { t } = useTranslation('ScanScreen');
const { appService } = useContext(GlobalContext);
const scanService = appService.children.get('scan');
const settingsService = appService.children.get('settings');
const navigation = useNavigation<ScanLayoutNavigation>();
const isLocationDisabled = useSelector(scanService, selectIsLocationDisabled);
const isLocationDenied = useSelector(scanService, selectIsLocationDenied);
const locationError = { message: '', button: '' };
if (isLocationDisabled) {
locationError.message = t('errors.locationDisabled.message');
locationError.button = t('errors.locationDisabled.button');
} else if (isLocationDenied) {
locationError.message = t('errors.locationDenied.message');
locationError.button = t('errors.locationDenied.button');
}
const isInvalid = useSelector(scanService, selectIsInvalid);
const isConnecting = useSelector(scanService, selectIsConnecting);
const isConnectingTimeout = useSelector(
scanService,
selectIsConnectingTimeout
);
const isExchangingDeviceInfo = useSelector(
scanService,
selectIsExchangingDeviceInfo
);
const isExchangingDeviceInfoTimeout = useSelector(
scanService,
selectIsExchangingDeviceInfoTimeout
);
const onCancel = () => scanService.send(ScanEvents.CANCEL());
let statusOverlay: Pick<
MessageOverlayProps,
'message' | 'hint' | 'onCancel'
> = null;
if (isConnecting) {
statusOverlay = {
message: t('status.connecting'),
};
} else if (isConnectingTimeout) {
statusOverlay = {
message: t('status.connecting'),
hint: t('status.connectingTimeout'),
onCancel,
};
} else if (isExchangingDeviceInfo) {
statusOverlay = {
message: t('status.exchangingDeviceInfo'),
};
} else if (isExchangingDeviceInfoTimeout) {
statusOverlay = {
message: t('status.exchangingDeviceInfo'),
hint: t('status.exchangingDeviceInfoTimeout'),
onCancel,
};
} else if (isInvalid) {
statusOverlay = {
message: t('status.invalid'),
};
}
useEffect(() => {
const subscriptions = [
navigation.addListener('focus', () =>
scanService.send(ScanEvents.SCREEN_FOCUS())
),
navigation.addListener('blur', () =>
scanService.send(ScanEvents.SCREEN_BLUR())
),
];
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
};
}, []);
const isDone = useSelector(scanService, selectIsDone);
const isReviewing = useSelector(scanService, selectIsReviewing);
const isScanning = useSelector(scanService, selectIsScanning);
useEffect(() => {
if (isDone) {
navigation.navigate('Home', { activeTab: 0 });
} else if (isReviewing) {
navigation.navigate('SendVcScreen');
} else if (isScanning) {
navigation.navigate('ScanScreen');
}
}, [isDone, isReviewing]);
return {
vcLabel: useSelector(settingsService, selectVcLabel),
isInvalid,
isDone,
statusOverlay,
DISMISS_INVALID: () =>
isInvalid ? scanService.send(ScanEvents.DISMISS()) : null,
};
}

View File

@@ -3,10 +3,6 @@
"noShareableVcs": "No shareable {{vcLabel}} are available.",
"sharingVc": "Sharing {{vcLabel}}",
"errors": {
"flightMode": {
"message": "Flight mode must be disabled for the scanning functionality",
"button": "Disable flight mode"
},
"locationDisabled": {
"message": "Location services must be enabled for the scanning functionality",
"button": "Enable location services"
@@ -18,7 +14,9 @@
},
"status": {
"connecting": "Connecting...",
"connectingTimeout": "It's taking a while to establish connection. Is the other device open for connections?",
"exchangingDeviceInfo": "Exchanging device info...",
"exchangingDeviceInfoTimeout": "It's taking a while to exchange device info. You may have to reconnect.",
"invalid": "Invalid QR Code"
}
}

View File

@@ -1,63 +1,52 @@
import React from 'react';
import { QrScanner } from '../../components/QrScanner';
import { Button, Column, Text } from '../../components/ui';
import { Theme } from '../../components/ui/styleUtils';
import { MainRouteProps } from '../../routes/main';
import { MessageOverlay } from '../../components/MessageOverlay';
import { useScanScreen } from './ScanScreenController';
import { useTranslation } from 'react-i18next';
import { SendVcModal } from './SendVcModal';
import { QrScanner } from '../../components/QrScanner';
import { Button, Centered, Column, Text } from '../../components/ui';
import { Theme } from '../../components/ui/styleUtils';
import { useScanScreen } from './ScanScreenController';
export const ScanScreen: React.FC<MainRouteProps> = (props) => {
export const ScanScreen: React.FC = () => {
const { t } = useTranslation('ScanScreen');
const controller = useScanScreen(props);
const controller = useScanScreen();
return (
<Column
fill
padding="98 24 24 24"
padding="24 0"
backgroundColor={Theme.Colors.lightGreyBackgroundColor}>
<Text align="center">{t('header')}</Text>
<Centered
fill
align="space-evenly"
backgroundColor={Theme.Colors.lightGreyBackgroundColor}>
<Text align="center">{t('header')}</Text>
{controller.isLocationDisabled ||
controller.isLocationDenied ||
controller.isFlightMode ? (
<Column fill align="space-between">
<Text align="center" margin="16 0" color={Theme.Colors.errorMessage}>
{controller.locationError.message}
</Text>
<Button
title={controller.locationError.button}
onPress={controller.ON_REQUEST}
/>
</Column>
) : null}
{!controller.isEmpty ? (
controller.isScanning && (
<Column fill padding="16 0" crossAlign="center">
<QrScanner onQrFound={controller.SCAN} />
{controller.isLocationDisabled || controller.isLocationDenied ? (
<Column align="space-between">
<Text
align="center"
margin="16 0"
color={Theme.Colors.errorMessage}>
{controller.locationError.message}
</Text>
<Button
title={controller.locationError.button}
onPress={controller.LOCATION_REQUEST}
/>
</Column>
)
) : (
<Text align="center" margin="16 0" color={Theme.Colors.errorMessage}>
{t('noShareableVcs', { vcLabel: controller.vcLabel.plural })}
</Text>
)}
) : null}
<MessageOverlay
isVisible={controller.statusMessage !== ''}
message={controller.statusMessage}
hasProgress={!controller.isInvalid}
onBackdropPress={controller.DISMISS_INVALID}
/>
<SendVcModal
isVisible={controller.isReviewing}
onDismiss={controller.DISMISS}
headerElevation={2}
headerTitle={t('sharingVc', { vcLabel: controller.vcLabel.singular })}
/>
{!controller.isEmpty ? (
controller.isScanning && (
<Column crossAlign="center">
<QrScanner onQrFound={controller.SCAN} />
</Column>
)
) : (
<Text align="center" color={Theme.Colors.errorMessage}>
{t('noShareableVcs', { vcLabel: controller.vcLabel.plural })}
</Text>
)}
</Centered>
</Column>
);
};

View File

@@ -1,23 +1,18 @@
import { useSelector } from '@xstate/react';
import { useContext, useEffect } from 'react';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import {
ScanEvents,
selectIsInvalid,
selectIsAirplaneEnabled,
selectIsLocationDisabled,
selectIsLocationDenied,
selectIsReviewing,
selectIsScanning,
selectIsConnecting,
selectIsExchangingDeviceInfo,
} from '../../machines/scan';
import { selectVcLabel } from '../../machines/settings';
import { selectShareableVcs } from '../../machines/vc';
import { MainRouteProps } from '../../routes/main';
import { GlobalContext } from '../../shared/GlobalContext';
export function useScanScreen({ navigation }: MainRouteProps) {
export function useScanScreen() {
const { t } = useTranslation('ScanScreen');
const { appService } = useContext(GlobalContext);
const scanService = appService.children.get('scan');
@@ -28,13 +23,10 @@ export function useScanScreen({ navigation }: MainRouteProps) {
const isLocationDisabled = useSelector(scanService, selectIsLocationDisabled);
const isLocationDenied = useSelector(scanService, selectIsLocationDenied);
const isFlightMode = useSelector(scanService, selectIsAirplaneEnabled);
const locationError = { message: '', button: '' };
if (isFlightMode) {
locationError.message = t('errors.flightMode.message');
locationError.button = t('errors.flightMode.button');
} else if (isLocationDisabled) {
if (isLocationDisabled) {
locationError.message = t('errors.locationDisabled.message');
locationError.button = t('errors.locationDisabled.button');
} else if (isLocationDenied) {
@@ -42,64 +34,16 @@ export function useScanScreen({ navigation }: MainRouteProps) {
locationError.button = t('errors.locationDenied.button');
}
const isInvalid = useSelector(scanService, selectIsInvalid);
const isConnecting = useSelector(scanService, selectIsConnecting);
const isExchangingDeviceInfo = useSelector(
scanService,
selectIsExchangingDeviceInfo
);
let statusMessage = '';
if (isConnecting) {
statusMessage = t('status.connecting');
} else if (isExchangingDeviceInfo) {
statusMessage = t('status.exchangingDeviceInfo');
} else if (isInvalid) {
statusMessage = t('status.invalid');
}
useEffect(() => {
const subscriptions = [
navigation.addListener('focus', () =>
scanService.send(ScanEvents.SCREEN_FOCUS())
),
navigation.addListener('blur', () =>
scanService.send(ScanEvents.SCREEN_BLUR())
),
];
const navSubscription = scanService.subscribe((state) => {
if (state.matches('reviewing.navigatingToHome')) {
navigation.navigate('Home', { activeTab: 0 });
}
});
return () => {
subscriptions.forEach((unsubscribe) => unsubscribe());
navSubscription.unsubscribe();
};
}, []);
return {
locationError,
vcLabel: useSelector(settingsService, selectVcLabel),
isInvalid,
isEmpty: !shareableVcs.length,
isLocationDisabled,
isLocationDenied,
isScanning: useSelector(scanService, selectIsScanning),
isReviewing: useSelector(scanService, selectIsReviewing),
isFlightMode,
statusMessage,
DISMISS: () => scanService.send(ScanEvents.DISMISS()),
ON_REQUEST: () =>
isFlightMode
? scanService.send(ScanEvents.FLIGHT_REQUEST())
: scanService.send(ScanEvents.LOCATION_REQUEST()),
LOCATION_REQUEST: () => scanService.send(ScanEvents.LOCATION_REQUEST()),
SCAN: (qrCode: string) => scanService.send(ScanEvents.SCAN(qrCode)),
DISMISS_INVALID: () =>
isInvalid ? scanService.send(ScanEvents.DISMISS()) : null,
};
}

View File

@@ -1,6 +1,6 @@
{
"header": "Share {{vcLabel}}",
"chooseVc": "Choose the {{vcLabel}} you'd like to share with",
"cancel": "Cancel",
"share": "Share"
"share": "Share",
"verifyAndShare": "Verify Identity & Share"
}

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { Dimensions, StyleSheet } from 'react-native';
import { Overlay } from 'react-native-elements/dist/overlay/Overlay';
import { Button, Column, Row, Text } from '../../components/ui';
import { Button, Column, Text } from '../../components/ui';
import { Theme } from '../../components/ui/styleUtils';
import { VcItem } from '../../components/VcItem';
import {
@@ -47,21 +47,25 @@ export const SelectVcOverlay: React.FC<SelectVcOverlayProps> = (props) => {
/>
))}
</Column>
<Row margin="16 0 0 0">
<Button
fill
type="clear"
title={t('cancel')}
onPress={() => props.onCancel()}
margin="0 8 0 0"
/>
<Button
fill
title={t('share')}
disabled={controller.selectedIndex == null}
onPress={controller.onSelect}
/>
</Row>
<Button
title={t('share')}
disabled={controller.selectedIndex == null}
onPress={controller.onSelect}
margin="8 0 0 0"
/>
<Button
type="outline"
title={t('verifyAndShare')}
disabled={controller.selectedIndex == null}
onPress={controller.onVerifyAndSelect}
margin="8 0 0 0"
/>
<Button
type="clear"
title={t('common:cancel')}
onPress={props.onCancel}
margin="8 0 0 0"
/>
</Column>
</Overlay>
);

View File

@@ -23,6 +23,11 @@ export function useSelectVcOverlay(props: SelectVcOverlayProps) {
const { serviceRefs, ...vc } = selectedVcRef.getSnapshot().context;
props.onSelect(vc);
},
onVerifyAndSelect: () => {
const { serviceRefs, ...vc } = selectedVcRef.getSnapshot().context;
props.onVerifyAndSelect(vc);
},
};
function selectVcItem(index: number) {
@@ -38,5 +43,6 @@ export interface SelectVcOverlayProps {
receiverName: string;
vcKeys: string[];
onSelect: (vc: VC) => void;
onVerifyAndSelect: (vc: VC) => void;
onCancel: () => void;
}

View File

@@ -1,16 +0,0 @@
{
"reasonForSharing": "Reason for sharing (optional)",
"acceptRequest": "Accept request and choose {{vcLabel}}",
"reject": "Reject",
"statusSharing": {
"title": "Sharing..."
},
"statusAccepted": {
"title": "Success!",
"message": "Your {{vcLabel}} has been successfully shared with {{receiver}}"
},
"statusRejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
}
}

View File

@@ -0,0 +1,26 @@
{
"reasonForSharing": "Reason for sharing (optional)",
"acceptRequest": "Accept request and choose {{vcLabel}}",
"reject": "Reject",
"status": {
"sharing": {
"title": "Sharing...",
"timeoutHint": "It's taking a while to share VC. There could be a problem with the connection."
},
"accepted": {
"title": "Success!",
"message": "Your {{vcLabel}} has been successfully shared with {{receiver}}"
},
"rejected": {
"title": "Notice",
"message": "Your {{vcLabel}} was rejected by {{receiver}}"
},
"verifyingIdentity": "Verifying identity..."
},
"errors": {
"invalidIdentity": {
"title": "Unable to verify identity",
"message": "An error occured and we couldn't scan your portrait. Try again, make sure your face is visible, devoid of any accessories."
}
}
}

View File

@@ -1,19 +1,19 @@
import React from 'react';
import { Input } from 'react-native-elements';
import { DeviceInfoList } from '../../components/DeviceInfoList';
import { Button, Column } from '../../components/ui';
import { Button, Column, Row } from '../../components/ui';
import { Theme } from '../../components/ui/styleUtils';
import { MessageOverlay } from '../../components/MessageOverlay';
import { Modal, ModalProps } from '../../components/ui/Modal';
import { useSendVcModal } from './SendVcModalController';
import { useSendVcScreen } from './SendVcScreenController';
import { useTranslation } from 'react-i18next';
import { VcItem } from '../../components/VcItem';
import { useSelectVcOverlay } from './SelectVcOverlayController';
import { SingleVcItem } from '../../components/SingleVcItem';
import { VerifyIdentityOverlay } from './VerifyIdentityOverlay';
export const SendVcModal: React.FC<SendVcModalProps> = (props) => {
const { t } = useTranslation('SendVcModal');
const controller = useSendVcModal();
export const SendVcScreen: React.FC = () => {
const { t } = useTranslation('SendVcScreen');
const controller = useSendVcScreen();
const onShare = () => {
controller.ACCEPT_REQUEST();
@@ -33,7 +33,7 @@ export const SendVcModal: React.FC<SendVcModalProps> = (props) => {
const reasonLabel = t('Reason For Sharing');
return (
<Modal {...props}>
<React.Fragment>
<Column fill backgroundColor={Theme.Colors.lightGreyBackgroundColor}>
<Column padding="16 0" scroll>
<DeviceInfoList of="receiver" deviceInfo={controller.receiverInfo} />
@@ -90,33 +90,80 @@ export const SendVcModal: React.FC<SendVcModalProps> = (props) => {
</Column>
</Column>
<SelectVcOverlay
isVisible={controller.isSelectingVc}
receiverName={controller.receiverInfo.deviceName}
onSelect={controller.SELECT_VC}
onVerifyAndSelect={controller.VERIFY_AND_SELECT_VC}
onCancel={controller.CANCEL}
vcKeys={controller.vcKeys}
/>
<VerifyIdentityOverlay
isVisible={controller.isVerifyingUserIdentity}
onCancel={controller.CANCEL}
onFaceValid={controller.FACE_VALID}
onFaceInvalid={controller.FACE_INVALID}
/>
<MessageOverlay
isVisible={controller.isInvalidUserIdentity}
title={t('errors.invalidIdentity.title')}
message={t('errors.invalidIdentity.message')}
onBackdropPress={controller.DISMISS}>
<Row>
<Button
fill
type="clear"
title={t('common:cancel')}
onPress={controller.DISMISS}
margin={[0, 8, 0, 0]}
/>
<Button
fill
title={t('common:tryAgain')}
onPress={controller.RETRY_VERIFICATION}
/>
</Row>
</MessageOverlay>
<MessageOverlay
isVisible={controller.isSendingVc}
title={t('Sharing..')}
hasProgress
title={t('status.sharing.title')}
hint={
controller.isSendingVcTimeout ? t('status.sharing.timeoutHint') : null
}
onCancel={controller.isSendingVcTimeout ? controller.CANCEL : null}
progress
/>
<MessageOverlay
isVisible={controller.status != null}
title={controller.status?.title}
hint={controller.status?.hint}
onCancel={controller.status?.onCancel}
progress
/>
<MessageOverlay
isVisible={controller.isAccepted}
title={t(controller.vcLabel.singular, 'Sent succesfully')}
message={t('statusAccepted.message', {
title={t('status.accepted.title')}
message={t('status.accepted.message', {
vcLabel: controller.vcLabel.singular,
receiver: controller.receiverInfo.deviceName,
})}
onShow={props.onDismiss}
onShow={controller.DISMISS}
/>
<MessageOverlay
isVisible={controller.isRejected}
title={t('statusRejected.title')}
message={t('statusRejected.message', {
title={t('status.rejected.title')}
message={t('status.rejected.message', {
vcLabel: controller.vcLabel.singular,
receiver: controller.receiverInfo.deviceName,
})}
onBackdropPress={props.onDismiss}
onBackdropPress={controller.DISMISS}
/>
</Modal>
</React.Fragment>
);
};
type SendVcModalProps = ModalProps;

View File

@@ -1,5 +1,7 @@
import { useSelector } from '@xstate/react';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { MessageOverlayProps } from '../../components/MessageOverlay';
import {
ScanEvents,
selectIsAccepted,
@@ -9,19 +11,41 @@ import {
selectIsSelectingVc,
selectIsSendingVc,
selectVcName,
selectIsSendingVcTimeout,
selectIsVerifyingUserIdentity,
selectIsInvalidUserIdentity,
} from '../../machines/scan';
import { selectVcLabel } from '../../machines/settings';
import { selectShareableVcs } from '../../machines/vc';
import { GlobalContext } from '../../shared/GlobalContext';
import { VC } from '../../types/vc';
export function useSendVcModal() {
export function useSendVcScreen() {
const { appService } = useContext(GlobalContext);
const scanService = appService.children.get('scan');
const settingsService = appService.children.get('settings');
const vcService = appService.children.get('vc');
const { t } = useTranslation('SendVcScreen');
const isSendingVc = useSelector(scanService, selectIsSendingVc);
const isSendingVcTimeout = useSelector(scanService, selectIsSendingVcTimeout);
const CANCEL = () => scanService.send(ScanEvents.CANCEL());
let status: Pick<MessageOverlayProps, 'title' | 'hint' | 'onCancel'> = null;
if (isSendingVc) {
status = {
title: t('status.sharing.title'),
};
} else if (isSendingVcTimeout) {
status = {
title: t('status.sharing.title'),
hint: t('status.sharing.timeoutHint'),
onCancel: CANCEL,
};
}
return {
status,
receiverInfo: useSelector(scanService, selectReceiverInfo),
reason: useSelector(scanService, selectReason),
vcName: useSelector(scanService, selectVcName),
@@ -29,17 +53,31 @@ export function useSendVcModal() {
vcKeys: useSelector(vcService, selectShareableVcs),
isSelectingVc: useSelector(scanService, selectIsSelectingVc),
isSendingVc: useSelector(scanService, selectIsSendingVc),
isSendingVc,
isSendingVcTimeout,
isAccepted: useSelector(scanService, selectIsAccepted),
isRejected: useSelector(scanService, selectIsRejected),
isVerifyingUserIdentity: useSelector(
scanService,
selectIsVerifyingUserIdentity
),
isInvalidUserIdentity: useSelector(
scanService,
selectIsInvalidUserIdentity
),
ACCEPT_REQUEST: () => scanService.send(ScanEvents.ACCEPT_REQUEST()),
CANCEL: () => scanService.send(ScanEvents.CANCEL()),
CANCEL,
SELECT_VC: (vc: VC) => scanService.send(ScanEvents.SELECT_VC(vc)),
VERIFY_AND_SELECT_VC: (vc: VC) =>
scanService.send(ScanEvents.VERIFY_AND_SELECT_VC(vc)),
DISMISS: () => scanService.send(ScanEvents.DISMISS()),
UPDATE_REASON: (reason: string) =>
scanService.send(ScanEvents.UPDATE_REASON(reason)),
UPDATE_VC_NAME: (vcName: string) =>
scanService.send(ScanEvents.UPDATE_VC_NAME(vcName)),
FACE_VALID: () => scanService.send(ScanEvents.FACE_VALID()),
FACE_INVALID: () => scanService.send(ScanEvents.FACE_INVALID()),
RETRY_VERIFICATION: () => scanService.send(ScanEvents.RETRY_VERIFICATION()),
};
}

View File

@@ -0,0 +1,39 @@
import FaceAuth from 'mosip-mobileid-sdk';
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { Icon, Overlay } from 'react-native-elements';
import { Column, Row } from '../../components/ui';
import { Colors } from '../../components/ui/styleUtils';
import {
useVerifyIdentityOverlay,
VerifyIdentityOverlayProps,
} from './VerifyIdentityOverlayController';
const styles = StyleSheet.create({
content: {
width: Dimensions.get('screen').width,
height: Dimensions.get('screen').height,
backgroundColor: Colors.White,
},
});
export const VerifyIdentityOverlay: React.FC<VerifyIdentityOverlayProps> = (
props
) => {
const controller = useVerifyIdentityOverlay();
return (
<Overlay isVisible={props.isVisible}>
<Row align="flex-end" padding="16">
<Icon name="close" color={Colors.Orange} onPress={props.onCancel} />
</Row>
<Column fill style={styles.content} align="center">
<FaceAuth
data={controller.selectedVc?.credential?.biometrics.face}
onValidationSuccess={props.onFaceValid}
// onValidationFailed={props.onFaceInvalid}
/>
</Column>
</Overlay>
);
};

View File

@@ -0,0 +1,21 @@
import { useContext } from 'react';
import { scanMachine, selectSelectedVc } from '../../machines/scan';
import { GlobalContext } from '../../shared/GlobalContext';
import { useSelector } from '@xstate/react';
export function useVerifyIdentityOverlay() {
const { appService } = useContext(GlobalContext);
const scanService = appService.children.get(scanMachine.id);
return {
selectedVc: useSelector(scanService, selectSelectedVc),
};
}
export interface VerifyIdentityOverlayProps {
isVisible: boolean;
onCancel: () => void;
onFaceValid: () => void;
onFaceInvalid: () => void;
}

View File

@@ -8,6 +8,7 @@ import { scanMachine } from '../machines/scan';
import { settingsMachine } from '../machines/settings';
import { storeMachine } from '../machines/store';
import { vcMachine } from '../machines/vc';
import { revokeVidsMachine } from '../machines/revoke';
export const GlobalContext = createContext({} as GlobalServices);
@@ -23,4 +24,5 @@ export interface AppServices {
activityLog: ActorRefFrom<typeof activityLogMachine>;
request: ActorRefFrom<typeof requestMachine>;
scan: ActorRefFrom<typeof scanMachine>;
revoke: ActorRefFrom<typeof revokeVidsMachine>;
}

View File

@@ -1,5 +1,6 @@
{
"cancel": "Cancel",
"save": "Save",
"editLabel": "Edit {{label}}"
"editLabel": "Edit {{label}}",
"tryAgain": "Try again"
}

7
shared/location.ios.ts Normal file
View File

@@ -0,0 +1,7 @@
export function checkLocation(onEnabled: () => void) {
onEnabled(); // iOS does not need location enabled
}
export function requestLocation() {
// pass
}

19
shared/location.ts Normal file
View File

@@ -0,0 +1,19 @@
import LocationEnabler from 'react-native-location-enabler';
const LOCATION_CONFIG = {
priority: LocationEnabler.PRIORITIES.BALANCED_POWER_ACCURACY,
alwaysShow: false,
needBle: true,
};
export function checkLocation(onEnabled: () => void, onDisabled: () => void) {
const subscription = LocationEnabler.addListener(({ locationEnabled }) => {
locationEnabled ? onEnabled() : onDisabled();
});
LocationEnabler.checkSettings(LOCATION_CONFIG);
return subscription;
}
export function requestLocation() {
return LocationEnabler.requestResolutionSettings(LOCATION_CONFIG);
}

View File

@@ -9,7 +9,7 @@ export class BackendResponseError extends Error {
}
export async function request(
method: 'GET' | 'POST',
method: 'GET' | 'POST' | 'PATCH',
path: `/${string}`,
body?: Record<string, unknown>
) {

View File

@@ -0,0 +1,29 @@
import vcjs from '@digitalcredentials/vc';
import jsonld from '@digitalcredentials/jsonld';
// import { RSAKeyPair } from '@digitalcredentials/jsonld-signatures';
import { RsaSignature2018 } from '../../lib/jsonld-signatures/suites/rsa2018/RsaSignature2018';
import { VerifiableCredential, VerifiablePresentation } from '../../types/vc';
export function createVerifiablePresentation(
vc: VerifiableCredential,
challenge: string
): Promise<VerifiablePresentation> {
const presentation = vcjs.createPresentation({
verifiableCredential: [vc],
});
// TODO: private key to sign VP
// const key = new RSAKeyPair({ ... })
const suite = new RsaSignature2018({
verificationMethod: vc.proof.verificationMethod,
date: vc.proof.created,
// TODO: key
});
return vcjs.signPresentation({
presentation,
suite,
challenge,
documentLoader: jsonld.documentLoaders.xhr(),
});
}

View File

@@ -1,10 +1,10 @@
import vcjs from '@digitalcredentials/vc';
import jsonld from '@digitalcredentials/jsonld';
import { RsaSignature2018 } from '../lib/jsonld-signatures/suites/rsa2018/RsaSignature2018';
import { Ed25519Signature2018 } from '../lib/jsonld-signatures/suites/ed255192018/Ed25519Signature2018';
import { AssertionProofPurpose } from '../lib/jsonld-signatures/purposes/AssertionProofPurpose';
import { PublicKeyProofPurpose } from '../lib/jsonld-signatures/purposes/PublicKeyProofPurpose';
import { VerifiableCredential } from '../types/vc';
import { RsaSignature2018 } from '../../lib/jsonld-signatures/suites/rsa2018/RsaSignature2018';
import { Ed25519Signature2018 } from '../../lib/jsonld-signatures/suites/ed255192018/Ed25519Signature2018';
import { AssertionProofPurpose } from '../../lib/jsonld-signatures/purposes/AssertionProofPurpose';
import { PublicKeyProofPurpose } from '../../lib/jsonld-signatures/purposes/PublicKeyProofPurpose';
import { VerifiableCredential } from '../../types/vc';
// FIXME: Ed25519Signature2018 not fully supported yet.
const ProofType = {

View File

@@ -0,0 +1,23 @@
import vcjs from '@digitalcredentials/vc';
import jsonld from '@digitalcredentials/jsonld';
import { RsaSignature2018 } from '../../lib/jsonld-signatures/suites/rsa2018/RsaSignature2018';
import { VerifiablePresentation } from '../../types/vc';
export async function verifyPresentation(
presentation: VerifiablePresentation,
challenge: string
): Promise<boolean> {
const suite = new RsaSignature2018({
verificationMethod: presentation.proof.verificationMethod,
date: presentation.proof.created,
});
const result = await vcjs.verify({
presentation,
challenge,
suite,
documentLoader: jsonld.documentLoaders.xhr(),
});
return result.verified;
}

View File

@@ -31,25 +31,27 @@ export interface DecodedCredential {
export interface CredentialSubject {
UIN: string;
addressLine1: string;
addressLine2: string;
addressLine3: string;
addressLine1: LocalizedField[] | string;
addressLine2: LocalizedField[] | string;
addressLine3: LocalizedField[] | string;
biometrics: string; // Encrypted Base64Encoded Biometrics
city: string;
city: LocalizedField[] | string;
dateOfBirth: string;
email: string;
fullName: string;
gender: string;
gender: LocalizedField[] | string;
id: string;
phone: string;
postalCode: string;
province: string;
region: string;
province: LocalizedField[] | string;
region: LocalizedField[] | string;
vcVer: 'VC-V1' | string;
}
type VCContext = (string | Record<string, unknown>)[];
export interface VerifiableCredential {
'@context': (string | Record<string, unknown>)[];
'@context': VCContext;
'credentialSubject': CredentialSubject;
'id': string;
'issuanceDate': string;
@@ -64,6 +66,21 @@ export interface VerifiableCredential {
'type': VerifiableCredentialType[];
}
export interface VerifiablePresentation {
'@context': VCContext;
'verifiableCredential': VerifiableCredential[];
'type': 'VerifiablePresentation';
'proof': {
created: string;
jws: string;
proofPurpose: 'authentication' | string;
type: 'RsaSignature2018' | string;
verificationMethod: string;
challenge: string;
domain: string;
};
}
export type VerifiableCredentialType =
| 'VerifiableCredential'
| 'MOSIPVerfiableCredential'
@@ -73,3 +90,8 @@ export interface VCLabel {
singular: string;
plural: string;
}
export interface LocalizedField {
language: string;
value: string;
}