Skip to content
Snippets Groups Projects
Commit 124d1605 authored by Alexey Lunin's avatar Alexey Lunin
Browse files

PinEnter Avatar ListContacts ListCredentials Title screen and components

parent a9c3abe6
No related branches found
No related tags found
1 merge request!9Refactor App and upgrade AFJ to 4.0
Showing
with 812 additions and 20 deletions
import { render } from '@testing-library/react-native';
import React from 'react';
import AvatarView from './index';
describe('AvatarView', () => {
it('should render avatarText with first character of the title', () => {
const { getByText } = render(<AvatarView name="avatar" />);
const avatarText = getByText('a');
expect(avatarText.props.children).toBe('a');
});
it('should render avatarText with first character of the title with single character', () => {
const { getByText } = render(<AvatarView name="K" />);
const avatarText = getByText('K');
expect(avatarText.props.children).toBe('K');
});
});
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { borderRadius, TextTheme } from 'src/theme/theme';
import { hashToRGBA, hashCode } from 'src/utils/helpers';
import IdSvg from 'src/assets/svg/id.svg';
interface AvatarViewProps {
name: string;
}
const AvatarView: React.FC<AvatarViewProps> = ({ name }) => {
return (
<View
style={[styles.avatar, { backgroundColor: hashToRGBA(hashCode(name)) }]}
>
<IdSvg style={styles.svg} />
</View>
);
};
export default AvatarView;
const styles = StyleSheet.create({
avatar: {
width: 80,
height: 80,
justifyContent: 'center',
alignItems: 'center',
borderRadius: borderRadius,
},
svg: {
width: 66,
height: 66,
},
});
import React from 'react';
import {
CredentialMetadataKeys,
V1CredentialPreview,
CredentialExchangeRecord,
CredentialState,
} from '@aries-framework/core';
import { Alert } from 'react-native';
import { render } from '@testing-library/react-native';
import { parsedSchema } from 'src/utils/helpers';
import CredentialCard from './index';
const credentialRecord = new CredentialExchangeRecord({
connectionId: '28790bfe-1345-4c64-b21a-7d98982b3894',
threadId: 'threadId',
state: CredentialState.Done,
credentialAttributes: [
new V1CredentialPreview({
name: 'age',
value: '25',
}),
],
});
credentialRecord.metadata.set(CredentialMetadataKeys.IndyCredential, {
credentialDefinitionId: 'Th7MpTaRZVRYnPiabds81Y:3:CL:17:TA',
schemaId: 'TL1EaPFCZ8Si5aUrqScBDt:2:testschema:1.0',
});
describe('CredentialCard', () => {
jest.mock('react-native', () => {
const RN = jest.requireActual('react-native');
return Object.setPrototypeOf(
{
Alert: {
...RN.Alert,
alert: jest.fn(),
},
},
RN,
);
});
it('testing', () => {
// Alert.alert = jest.genMockFunction();
Alert.alert = jest.fn();
const { getByText } = render(
<CredentialCard credential={credentialRecord} />,
);
const name = getByText(parsedSchema(credentialRecord).name);
expect(name.props.children).toBe(parsedSchema(credentialRecord).name);
});
});
import { CredentialExchangeRecord } from '@aries-framework/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { StyleSheet, Text, View } from 'react-native';
import Title from 'src/components/Title';
import { DATE_TIME_FORMAT } from 'src/constants/constants';
import { borderRadius, ColorPallet, TextTheme } from 'src/theme/theme';
import { Shadow } from 'react-native-shadow-2';
import { parsedSchema } from 'src/utils/helpers';
import AvatarView from 'src/components/AvatarView';
import { format } from 'date-fns';
interface CredentialCardProps {
credential: CredentialExchangeRecord;
}
const CredentialCard: React.FC<CredentialCardProps> = ({ credential }) => {
const { t } = useTranslation();
const credParsed = parsedSchema(credential);
return (
<Shadow style={styles.container}>
<AvatarView name={credParsed.name} />
<View style={styles.details}>
<Title>{credParsed.name}</Title>
<Text style={TextTheme.caption}>
{t<string>('CredentialDetails.Issued')}:{' '}
{format(credential.createdAt, DATE_TIME_FORMAT)}
</Text>
</View>
</Shadow>
);
};
export default CredentialCard;
const styles = StyleSheet.create({
container: {
width: '100%',
backgroundColor: ColorPallet.grayscale.white,
borderRadius: borderRadius,
padding: 10,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'flex-start',
},
details: {
marginLeft: 24,
flexShrink: 1,
justifyContent: 'flex-start',
},
});
import React from 'react';
import { Text, StyleSheet, View } from 'react-native';
import { useTranslation } from "react-i18next";
import { ColorPallet } from 'src/theme/theme';
export interface NoRecordsAvailableProps {
text?: string;
}
const NoRecordsAvailable: React.FC<NoRecordsAvailableProps> = ({ text }) => {
const { t } = useTranslation();
const defaultText = t<string>('Global.ZeroRecords');
return (
<View style={styles.container}>
<Text
style={styles.title}
testID="Title"
accessibilityLabel={text || defaultText}
>
{text || defaultText}
</Text>
</View>
);
};
export default NoRecordsAvailable;
const styles = StyleSheet.create({
container: {
marginTop: 40,
marginBottom: 20,
alignItems: 'center',
justifyContent: 'center',
},
title: {
fontWeight: 'bold',
fontSize: 16,
color: ColorPallet.baseColors.black,
},
});
import React, { useState } from 'react';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useTranslation } from "react-i18next";
import {
Pressable,
StyleSheet,
View,
Keyboard,
TextInput,
Text,
} from 'react-native';
import { StackScreenProps } from '@react-navigation/stack';
import { useNavigation } from '@react-navigation/core';
import { borderRadius, ColorPallet, TextTheme } from 'src/theme/theme';
import useNotifications from 'src/hooks/notifications';
import SettingsSvg from 'src/assets/svg/settings.svg';
import SearchSvg from 'src/assets/svg/search.svg';
import BellSvg from 'src/assets/svg/bell.svg';
import { SettingStackParams, Stacks } from 'src/type/navigators';
interface Props {
searchPhrase: string;
setSearchPhrase: (text: string) => void;
}
const SearchBar: React.FC<Props> = ({ searchPhrase, setSearchPhrase }) => {
const { t } = useTranslation();
const [focused, setFocused] = useState(false);
const navigation = useNavigation<StackScreenProps<SettingStackParams>>();
const notifications = useNotifications();
const cancel = () => {
Keyboard.dismiss();
setFocused(false);
setSearchPhrase('');
};
return (
<View style={styles.container}>
<Pressable
onPress={() => {
navigation.navigate(Stacks.SettingsStack);
}}
>
<SettingsSvg style={styles.settingsIcon} />
</Pressable>
<View style={styles.searchBar}>
{focused && (
<Icon
name="close"
color={ColorPallet.grayscale.darkGrey}
size={24}
style={styles.closeIcon}
onPress={cancel}
/>
)}
<TextInput
style={[styles.input, focused ? styles.inputFocused : undefined]}
returnKeyType="done"
placeholder={t<string>('SearchBar.placeholder')}
value={searchPhrase}
placeholderTextColor={ColorPallet.baseColors.lightGrey}
onChangeText={setSearchPhrase}
onBlur={() => {
if (searchPhrase.trim() === '') {
cancel();
}
}}
onFocus={() => {
setFocused(true);
}}
/>
<SearchSvg style={styles.searchIcon} />
</View>
<Pressable
style={styles.bellContainer}
onPress={() => {
navigation.navigate(Stacks.NotificationStack);
}}
>
<BellSvg style={styles.bellIcon} />
{notifications.length > 0 && (
<View style={styles.notificationBadge}>
<Text style={styles.badgeText}>{notifications.length < 100 ? notifications.length : 99}</Text>
</View>
)}
</Pressable>
</View>
);
};
export default SearchBar;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
},
settingsIcon: {
width: 32,
height: 32,
fill: ColorPallet.baseColors.black,
alignSelf: 'center',
},
closeIcon: {
marginTop: 8,
marginLeft: 6,
},
searchIcon: {
width: 20,
height: 20,
fill: ColorPallet.baseColors.black,
alignSelf: 'center',
marginRight: 10,
},
searchBar: {
marginLeft: 15,
marginRight: 15,
height: 40,
flexDirection: 'row',
flex: 1,
backgroundColor: ColorPallet.grayscale.veryLightGrey,
borderRadius: borderRadius,
},
input: {
marginLeft: 8,
fontSize: 18,
alignSelf: 'center',
color: ColorPallet.grayscale.mediumGrey,
flex: 1,
},
inputFocused: {
marginLeft: 0,
},
bellContainer: {
position: 'relative',
flexDirection: 'row',
},
bellIcon: {
width: 32,
height: 32,
fill: ColorPallet.baseColors.black,
alignSelf: 'center',
},
notificationBadge: {
position: 'absolute',
right: -4,
top: 14,
width: 20,
height: 20,
backgroundColor: '#d51e32',
borderRadius: 24,
alignItems: 'center',
justifyContent: 'center',
},
badgeText: {
...TextTheme.label,
fontSize: 12,
color: ColorPallet.baseColors.white,
},
});
import React from 'react';
import { Text, StyleSheet, TextStyle } from 'react-native';
import { ColorPallet } from 'src/theme/theme';
interface Props {
children?: React.ReactNode;
style?: TextStyle;
}
const Title: React.FC<Props> = ({ children, style }) => {
return (
<Text
style={[styles.title, style]}
testID="Title"
accessibilityLabel="Title"
>
{children}
</Text>
);
};
export default Title;
const styles = StyleSheet.create({
title: {
fontWeight: 'bold',
fontSize: 20,
color: ColorPallet.baseColors.black,
},
});
import { useEffect, useState } from 'react';
import {getNotifications, Notification} from "../utils/agentUtils";
import rootStore from "../store/rootStore";
const useNotifications = (): Notification[] => {
const [notifications, setNotifications] = useState<Notification[]>([]);
const fetchNotifications = async () => {
const data = await getNotifications(rootStore.agentStore.agent);
setNotifications(data);
}
useEffect(() => {
const interval = setInterval(fetchNotifications, 2000);
return () => clearInterval(interval);
}, []);
return notifications;
};
export default useNotifications;
import React from 'react';
import type { ConnectionRecord } from '@aries-framework/core';
import { Pressable, StyleSheet, View } from 'react-native';
import { format } from 'date-fns';
import { useTranslation } from "react-i18next";
import { borderRadius, ColorPallet } from 'src/theme/theme';
import { DATE_TIME_FORMAT } from 'src/constants/constants';
import Text from 'src/components/Text';
import Title from 'src/components/Title';
interface Props {
contact: ConnectionRecord;
onPress: () => void;
}
const ContactListItem: React.FC<Props> = ({ contact, onPress }) => {
const { t } = useTranslation();
return (
<Pressable
testID="contact-list-item"
onPress={onPress}
key={contact.id}
style={styles.container}
>
<Title style={styles.title}>
{contact?.alias || contact?.theirLabel}
</Title>
<Text>{t<string>('List.ContactItem.Did')}: {contact.did}</Text>
<View style={styles.info}>
<View>
<Text>{t<string>('List.ContactItem.State')}: {contact.state}</Text>
</View>
<View>
<Text style={styles.date}>
{format(contact.createdAt, DATE_TIME_FORMAT)}
</Text>
</View>
</View>
</Pressable>
);
};
export default ContactListItem;
const styles = StyleSheet.create({
container: {
marginTop: 15,
padding: 15,
borderRadius,
backgroundColor: ColorPallet.grayscale.veryLightGrey,
},
title: {
color: ColorPallet.brand.primary,
marginBottom: 8,
},
info: {
marginTop: 8,
flexDirection: 'row',
justifyContent: 'space-between',
},
date: {
textAlign: 'right',
},
});
import {action, makeAutoObservable, runInAction} from "mobx";
import rootStore from "src/store/rootStore";
import {ConnectionRecord} from "@aries-framework/core/build/modules/connections/repository/ConnectionRecord";
class ListContactsStore {
private connections: ConnectionRecord[] = [];
public filteredConnections: ConnectionRecord[] = [];
public searchQuery = '';
constructor() {
makeAutoObservable(this);
}
public init = async () => {
const records = await rootStore.agentStore.agent.connections.getAll();
runInAction(() => {
this.connections = records;
this.filteredConnections = records;
});
};
public updateSearchQuery = action((query: string) => {
this.searchQuery = query;
this.filteredConnections = this.connections.filter(item => {
if (!item.theirLabel) {
return false;
}
const label = item.theirLabel.toUpperCase();
const text = query.toUpperCase();
return label.includes(text);
});
});
}
export type { ListContactsStore };
export default ListContactsStore;
import React from 'react';
import {StatusBar, View} from 'react-native';
import React, { useEffect, useState} from 'react';
import { FlatList, StyleSheet, View, StatusBar } from 'react-native';
import { ConnectionRecord } from '@aries-framework/core';
import {useNavigation} from "@react-navigation/core";
import { useTranslation } from 'react-i18next';
import SearchBar from 'src/components/SearchBar';
import { ColorPallet } from 'src/theme/theme';
import {observer} from "mobx-react";
import {Screens} from "src/type/navigators";
import NoRecordsAvailable from 'src/components/NoRecordsAvailable';
import ContactListItem from './ContactListItem';
import ListContactsStore from "./ListContactsStore";
const ListContacts: React.FC = observer(() => {
const [store] = useState(() => new ListContactsStore());
const navigation = useNavigation();
const { t } = useTranslation();
useEffect(() => {
store.init();
}, []);
const Empty = () => {
console.log('ListContacts');
return (
<View>
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<SearchBar
searchPhrase={store.searchQuery}
setSearchPhrase={text => store.updateSearchQuery(text)}
/>
{!store.filteredConnections.length && <NoRecordsAvailable />}
{!store.filteredConnections.length && (
<FlatList
data={store.filteredConnections}
renderItem={({ item }) => (
<ContactListItem
key={item.id}
contact={item}
onPress={() => {
navigation.navigate(Screens.ContactDetails, {
connectionId: item.id,
})
}}
/>
)}
keyExtractor={(item: ConnectionRecord) => item.id}
/>
)}
</View>
);
};
});
export default ListContacts;
export default Empty;
const styles = StyleSheet.create({
container: {
backgroundColor: ColorPallet.grayscale.white,
margin: 20,
flex: 1,
},
});
import {action, makeAutoObservable, runInAction} from "mobx";
import rootStore from "src/store/rootStore";
import {CredentialState} from "@aries-framework/core";
import {
CredentialExchangeRecord
} from "@aries-framework/core/build/modules/credentials/repository/CredentialExchangeRecord";
import {parsedSchema} from "../../utils/helpers";
class ListCredentialsStore {
private _credentials: CredentialExchangeRecord[] = [];
public filteredCredentials: CredentialExchangeRecord[] = [];
public searchQuery = '';
constructor() {
makeAutoObservable(this);
}
public init = async () => {
const records = await rootStore.agentStore.agent.credentials.findAllByQuery({
state: CredentialState.Done
});
runInAction(() => {
this._credentials = records;
this.filteredCredentials = records;
});
};
public updateSearchQuery = action((query: string) => {
this.searchQuery = query;
this.filteredCredentials = this._credentials.filter(item => {
const orgLabel = parsedSchema(item).name.toUpperCase();
const textData = query.toUpperCase();
return orgLabel.indexOf(textData) > -1;
});
});
}
export type { ListCredentialsStore };
export default ListCredentialsStore;
import React from 'react';
import {StatusBar, View} from 'react-native';
import { CredentialExchangeRecord } from '@aries-framework/core';
import React, { useEffect, useState } from 'react';
import {FlatList, View, StyleSheet, Pressable, StatusBar} from 'react-native';
import {observer} from "mobx-react";
import { useNavigation } from '@react-navigation/core';
import { ColorPallet, borderRadius } from 'src/theme/theme';
import Title from 'src/components/Title';
import SearchBar from 'src/components/SearchBar';
import ListCredentialsStore from "./ListCredentialsStore";
import {StackNavigationProp} from "@react-navigation/stack";
import CredentialCard from 'src/components/CredentialCard';
import {CredentialStackParams, Screens} from "src/type/navigators";
import NoRecordsAvailable from "src/components/NoRecordsAvailable";
const ListCredentials: React.FC = observer(() => {
const navigation = useNavigation<StackNavigationProp<CredentialStackParams>>();
const [store] = useState(() => new ListCredentialsStore())
useEffect(() => {
store.init();
}, []);
const Empty = () => {
console.log('ListCredentials');
return (
<View>
<View style={styles.container}>
<StatusBar barStyle="light-content" />
<SearchBar
searchPhrase={store.searchQuery}
setSearchPhrase={text => store.updateSearchQuery(text)}
/>
<Title>Your Credentials</Title>
{!store.filteredCredentials.length && <NoRecordsAvailable />}
{store.filteredCredentials.length && (
<FlatList
data={store.filteredCredentials}
keyExtractor={(item: CredentialExchangeRecord) => item?.id}
style={styles.list}
renderItem={({ item, index }) => (
<View
style={{
marginHorizontal: 10,
marginTop: 15,
marginBottom: index === store.filteredCredentials.length - 1 ? 15 : 0,
}}
key={item.id}
>
<Pressable
testID="credential-list-item"
onPress={() =>
navigation.navigate(Screens.CredentialDetails, {
credentialId: item.id,
})
}
>
<CredentialCard credential={item} />
</Pressable>
</View>
)}
/>
)}
</View>
);
};
});
export default ListCredentials;
export default Empty;
const styles = StyleSheet.create({
container: {
backgroundColor: ColorPallet.grayscale.white,
margin: 20,
flex: 1,
},
list: {
backgroundColor: ColorPallet.grayscale.veryLightGrey,
borderRadius: borderRadius,
},
});
import ReactNativeBiometrics from 'react-native-biometrics';
import i18next from 'i18next';
const rnBiometrics = new ReactNativeBiometrics();
export const checkIfSensorAvailable = async () => {
const result = await rnBiometrics.isSensorAvailable();
return result;
};
export const showBiometricPrompt = async () => {
const result = await rnBiometrics.simplePrompt({
promptMessage: i18next.t<string>('Biometric.BiometricConfirm'),
});
return result;
};
import React from 'react';
import {StatusBar, View} from 'react-native';
import React, { useState, useEffect, useCallback } from 'react';
import {
Alert,
BackHandler,
Keyboard,
StyleSheet,
View,
StatusBar
} from 'react-native';
import { useTranslation } from 'react-i18next';
import { StackScreenProps } from '@react-navigation/stack';
import { useFocusEffect } from '@react-navigation/native';
import TextInput from 'src/components/TextInput';
import Loader from 'src/components/Loader';
import Button, { ButtonType } from 'src/components/Button';
import { ColorPallet, TextTheme } from 'src/theme/theme';
import { OnboardingStackParams, Screens } from 'src/type/navigators';
import { checkIfSensorAvailable, showBiometricPrompt } from './PinEnter.utils';
import { warningToast } from 'src/utils/toast';
import {getPasscode} from "src/utils/keychain";
import {restoreDefault} from "src/utils/onboarding";
import rootStore from "src/store/rootStore";
type PinEnterProps = StackScreenProps<OnboardingStackParams, Screens.EnterPin>;
const PinEnter: React.FC<PinEnterProps> = ({ navigation }) => {
const [pin, setPin] = useState('');
const [loginAttemptsFailed, setLoginAttemptsFailed] = useState(0);
const [loading, setLoading] = useState(false);
const { t } = useTranslation();
useFocusEffect(
useCallback(() => {
const onBackPress = () => {
BackHandler.exitApp();
return true;
};
BackHandler.addEventListener('hardwareBackPress', onBackPress);
return () =>
BackHandler.removeEventListener('hardwareBackPress', onBackPress);
}, []),
);
useEffect(() => {
(async () => {
const { available } = await checkIfSensorAvailable();
if (!available) return;
const { success, error } = await showBiometricPrompt();
if (success) {
setLoading(true);
await rootStore.agentStore.startAgent();
await rootStore.setAuthenticated(true);
setLoading(false);
} else if (error) {
warningToast(t<string>('Biometric.BiometricCancel'));
}
})();
}, [t]);
const checkPin = async (pin: string) => {
const passcode = await getPasscode();
if (pin === passcode) {
setLoading(true);
await rootStore.agentStore.startAgent();
await rootStore.setAuthenticated(true);
setLoading(false);
} else {
warningToast(t<string>('PinEnter.IncorrectPin'));
setLoginAttemptsFailed(loginAttemptsFailed + 1);
if (loginAttemptsFailed === 5) {
Alert.alert(t<string>('Registration.RegisterAgain'));
navigation.navigate(Screens.EnterPin);
await restoreDefault();
}
}
};
const Empty = () => {
console.log('PinEnter');
return (
<View>
<View style={[styles.container]}>
<StatusBar barStyle="light-content" />
<Loader loading={loading} />
<TextInput
label={t<string>('Global.EnterPin')}
accessible
accessibilityLabel={t<string>('Global.EnterPin')}
placeholder={t<string>('Global.SixDigitPin')}
maxLength={6}
keyboardType="numeric"
secureTextEntry
value={pin}
returnKeyType="done"
onChangeText={(pin: string) => {
setPin(pin.replace(/[^0-9]/g, ''));
if (pin.length === 6) {
Keyboard.dismiss();
}
}}
/>
<Button
title={t<string>('Global.Submit')}
buttonType={ButtonType.Primary}
onPress={() => checkPin(pin)}
/>
</View>
);
};
export default Empty;
export default PinEnter;
const styles = StyleSheet.create({
container: {
backgroundColor: ColorPallet.grayscale.white,
margin: 20,
},
bodyText: {
...TextTheme.normal,
flexShrink: 1,
},
verticalSpacer: {
marginVertical: 20,
textAlign: 'center',
},
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment