diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index f35b029..0bd47d6 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -24,11 +24,15 @@ PODS:
- React-Core
- EXKeepAwake (10.0.2):
- ExpoModulesCore
+ - EXLocalAuthentication (12.1.1):
+ - ExpoModulesCore
- Expo (44.0.6):
- ExpoModulesCore
- ExpoModulesCore (0.6.5):
- React-Core
- ReactCommon/turbomodule/core
+ - EXSecureStore (11.1.1):
+ - ExpoModulesCore
- EXSplashScreen (0.14.2):
- ExpoModulesCore
- React-Core
@@ -423,8 +427,10 @@ DEPENDENCIES:
- EXFont (from `../node_modules/expo-font/ios`)
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
+ - EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
- Expo (from `../node_modules/expo/ios`)
- ExpoModulesCore (from `../node_modules/expo-modules-core/ios`)
+ - EXSecureStore (from `../node_modules/expo-secure-store/ios`)
- EXSplashScreen (from `../node_modules/expo-splash-screen/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
@@ -535,10 +541,14 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-image-loader/ios"
EXKeepAwake:
:path: "../node_modules/expo-keep-awake/ios"
+ EXLocalAuthentication:
+ :path: "../node_modules/expo-local-authentication/ios"
Expo:
:path: "../node_modules/expo/ios"
ExpoModulesCore:
:path: "../node_modules/expo-modules-core/ios"
+ EXSecureStore:
+ :path: "../node_modules/expo-secure-store/ios"
EXSplashScreen:
:path: "../node_modules/expo-splash-screen/ios"
FBLazyVector:
@@ -643,8 +653,10 @@ SPEC CHECKSUMS:
EXFont: 2597c10ac85a69d348d44d7873eccf5a7576ef5e
EXImageLoader: 347b72c2ec2df65120ccec40ea65a4c4f24317ff
EXKeepAwake: bf48d7f740a5cd2befed6cf9a49911d385c6c47d
+ EXLocalAuthentication: 3c5f368ee954b79c3778158eb8000cbce4e2f8a2
Expo: 534e51e607aba8229293297da5585f4b26f50fa1
ExpoModulesCore: 32c0ccb47f477d330ee93db72505380adf0de09a
+ EXSecureStore: b80c74c5ee29d0160c2aace3fd9a24a5edc20015
EXSplashScreen: 21669e598804ee810547dbb6692c8deb5dd8dbf3
FBLazyVector: e5569e42a1c79ca00521846c223173a57aca1fe1
FBReactNativeSpec: fe08c1cd7e2e205718d77ad14b34957cce949b58
diff --git a/ios/TeliosMobile/Info.plist b/ios/TeliosMobile/Info.plist
index bd12d28..d2f95f6 100644
--- a/ios/TeliosMobile/Info.plist
+++ b/ios/TeliosMobile/Info.plist
@@ -65,5 +65,7 @@
UIViewControllerBasedStatusBarAppearance
+ NSFaceIDUsageDescription
+ Allow $(PRODUCT_NAME) to use FaceID
diff --git a/package.json b/package.json
index a6a8962..77c28e1 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,8 @@
"expo-barcode-scanner": "~11.2.0",
"expo-clipboard": "~2.1.0",
"expo-constants": "~13.0.1",
+ "expo-local-authentication": "~12.1.0",
+ "expo-secure-store": "~11.1.0",
"formik": "^2.2.9",
"hypercore": "^10.0.0-alpha.18",
"lodash": "^4.17.21",
diff --git a/src/hooks/useFirstLogin.tsx b/src/hooks/useFirstLogin.tsx
new file mode 100644
index 0000000..42e2d8e
--- /dev/null
+++ b/src/hooks/useFirstLogin.tsx
@@ -0,0 +1,38 @@
+import { useEffect } from 'react';
+import { Alert } from 'react-native';
+import { useSelector } from 'react-redux';
+import { useNavigation } from '@react-navigation/native';
+import { selectIsFirstSignIn } from '../store/selectors/account';
+import { isBiometricSupported } from '../util/biometric';
+
+export default () => {
+ const isFirstSignIn = useSelector(selectIsFirstSignIn);
+ const navigation = useNavigation();
+
+ useEffect(() => {
+ (async () => {
+ const isBiometricSupport = await isBiometricSupported();
+ if (isBiometricSupport && isFirstSignIn) {
+ Alert.alert(
+ 'Login with Biometric',
+ 'Would you want to use FaceID for login?',
+ [
+ {
+ text: 'No',
+ style: 'default',
+ },
+ {
+ text: 'Yes',
+ onPress: () => handleUseFaceID(),
+ style: 'default',
+ },
+ ],
+ );
+ }
+ })();
+ }, [isFirstSignIn]);
+
+ const handleUseFaceID = () => {
+ navigation.navigate('biometricSettings');
+ };
+};
diff --git a/src/navigators/Navigator.tsx b/src/navigators/Navigator.tsx
index 82d25a8..21f1765 100644
--- a/src/navigators/Navigator.tsx
+++ b/src/navigators/Navigator.tsx
@@ -36,6 +36,7 @@ import { AliasInfoScreen } from '../screens/AliasInfo';
import NewAliasRandom from '../screens/NewAliasRandom';
import ForgotPassword, { ForgotPasswordStackParams } from './ForgotPassword';
import { ProfileRoot } from './Profile';
+import BiometricSettings from '../screens/BiometricSettings/BiometricSettings';
import Sync, { SyncStackParams } from './Sync';
import backArrow from './utils/backArrow';
import { selectIsSignedIn } from '../store/selectors/account';
@@ -82,6 +83,7 @@ export type RootStackParams = {
folderId: number;
isUnread: boolean;
};
+ biometricSettings: undefined;
};
export type RegisterStackParams = {
@@ -115,6 +117,7 @@ export type ProfileStackParams = {
statistics: undefined;
syncNewDevice: undefined;
security: undefined;
+ biometricSettings: undefined;
planAndUsage: undefined;
};
@@ -267,6 +270,19 @@ function CoreScreen() {
})}
/>
+ ({
+ title: 'Biometric Settings',
+ headerLeft: () => (
+ navigation.goBack()}
+ />
+ ),
+ })}
+ />
>
) : (
<>
diff --git a/src/navigators/Profile.tsx b/src/navigators/Profile.tsx
index 9398d90..f6c05bf 100644
--- a/src/navigators/Profile.tsx
+++ b/src/navigators/Profile.tsx
@@ -9,6 +9,8 @@ import { NewContact } from '../screens/NewContact/NewContact';
import Security from '../screens/Security/Security';
import { ContactScreen } from '../screens/Contacts/Contacts';
import PlanAndUsage from '../screens/PlanAndUsage/PlanAndUsage';
+import backArrow from './utils/backArrow';
+import { colors } from '../util/colors';
export const ProfileStack = createNativeStackNavigator();
export const ProfileRoot = () => (
@@ -24,10 +26,11 @@ export const ProfileRoot = () => (
({
title: '',
+ ...backArrow({ navigation, color: colors.primaryDark }),
headerTransparent: true,
- }}
+ })}
/>
(
({
+ title: 'Plan & Usage',
+ ...backArrow({ navigation, color: colors.primaryDark }),
+ })}
/>
({
+ title: 'Security',
+ ...backArrow({ navigation, color: colors.primaryDark }),
+ })}
/>
({
+ title: 'Sync New Device',
+ ...backArrow({ navigation, color: colors.primaryDark }),
+ })}
/>
);
diff --git a/src/screens/BiometricSettings/BiometricSettings.tsx b/src/screens/BiometricSettings/BiometricSettings.tsx
new file mode 100644
index 0000000..334067a
--- /dev/null
+++ b/src/screens/BiometricSettings/BiometricSettings.tsx
@@ -0,0 +1,95 @@
+import React, { useEffect, useLayoutEffect, useState } from 'react';
+import { Switch, Text, View } from 'react-native';
+import * as LocalAuthentication from 'expo-local-authentication';
+import cloneDeep from 'lodash/cloneDeep';
+import backArrow from '../../navigators/utils/backArrow';
+import { updateBiometricUseStatus } from '../../store/account';
+import {
+ selectBiometricUseStatus,
+ selectLastLoggedUsername,
+} from '../../store/selectors/account';
+import { useAppDispatch, useAppSelector } from '../../hooks';
+import { storeAsyncStorageBiometricUseStatus } from '../../util/asyncStorage';
+import { isBiometricSupported } from '../../util/biometric';
+import { colors } from '../../util/colors';
+import styles from './styles';
+
+const BiometricSettings = ({ navigation }) => {
+ const [isBiometricSupport, setIsBiometricSupport] = useState(false);
+ const [usingStatus, setUsingStatus] = useState(false);
+ const dispatch = useAppDispatch();
+ const lastLoggedUsername = useAppSelector(selectLastLoggedUsername);
+ const biometricUseStatus = useAppSelector(selectBiometricUseStatus);
+
+ useLayoutEffect(() => {
+ navigation.setOptions({
+ ...backArrow({
+ navigation: navigation,
+ color: colors.primaryDark,
+ }),
+ });
+ }, []);
+
+ useEffect(() => {
+ (async () => {
+ const isBiometricSupportResult = await isBiometricSupported();
+ setIsBiometricSupport(isBiometricSupportResult);
+ if (lastLoggedUsername) {
+ setUsingStatus(!!biometricUseStatus?.[lastLoggedUsername]);
+ }
+ })();
+ }, [biometricUseStatus, lastLoggedUsername]);
+
+ const handleUsingStatusChange = async (updatedStatus: boolean) => {
+ setUsingStatus(updatedStatus);
+ if ((isBiometricSupport && lastLoggedUsername) || 1) {
+ await LocalAuthentication.authenticateAsync({
+ promptMessage:
+ 'To change Telios uses settings, you must have identity verification.',
+ cancelLabel: 'Cancel',
+ disableDeviceFallback: false,
+ }).then(async res => {
+ if (res?.success && lastLoggedUsername) {
+ const newBiometricUseStatus = cloneDeep(biometricUseStatus);
+ newBiometricUseStatus[lastLoggedUsername] = updatedStatus;
+ dispatch(updateBiometricUseStatus(newBiometricUseStatus));
+ await storeAsyncStorageBiometricUseStatus(
+ lastLoggedUsername,
+ updatedStatus,
+ );
+ }
+ });
+ }
+ };
+
+ return (
+
+
+
+ Using status
+ {!isBiometricSupport && (
+
+ {'Your device does not include this feature.'}
+
+ )}
+
+
+
+
+ {
+ 'Allows you to login to your Telios account with biometric verification. \n\nWhen you want to log in to your account after closing the Telios application, you must log in with biometric verification again. \n\n *Biometric verification allows you to log into your account using whatever security method is defined on your phone.'
+ }
+
+
+ );
+};
+
+export default BiometricSettings;
diff --git a/src/screens/BiometricSettings/styles.ts b/src/screens/BiometricSettings/styles.ts
new file mode 100644
index 0000000..c8a0304
--- /dev/null
+++ b/src/screens/BiometricSettings/styles.ts
@@ -0,0 +1,31 @@
+import { StyleSheet } from 'react-native';
+import { colors } from '../../util/colors';
+import { fonts } from '../../util/fonts';
+import { spacing } from '../../util/spacing';
+
+export default StyleSheet.create({
+ container: {
+ flex: 1,
+ padding: spacing.lg,
+ backgroundColor: colors.white,
+ },
+ settingContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ paddingBottom: spacing.md,
+ borderBottomWidth: 1,
+ borderColor: colors.skyBase,
+ },
+ settingText: {
+ ...fonts.large.bold,
+ },
+ description: {
+ ...fonts.tiny.regular,
+ marginTop: spacing.md,
+ },
+ errorText: {
+ ...fonts.tiny.regular,
+ color: colors.error,
+ },
+});
diff --git a/src/screens/Inbox/InboxScreen.tsx b/src/screens/Inbox/InboxScreen.tsx
index c9a2f61..8698021 100644
--- a/src/screens/Inbox/InboxScreen.tsx
+++ b/src/screens/Inbox/InboxScreen.tsx
@@ -18,6 +18,7 @@ import {
getMailByFolderUnread,
} from '../../store/thunks/email';
import ComposeButton from '../../components/ComposeButton/ComposeButton';
+import useFirstLogin from '../../hooks/useFirstLogin';
export type InboxScreenProps = CompositeScreenProps<
NativeStackScreenProps,
@@ -28,6 +29,7 @@ export const InboxScreen = () => {
const mailboxAddress = useAppSelector(selectMailBoxAddress);
const dispatch = useAppDispatch();
const folderId = FoldersId.inbox;
+ useFirstLogin();
return (
<>
diff --git a/src/screens/LoginScreen.tsx b/src/screens/LoginScreen.tsx
index 7ecf15b..f56cef2 100644
--- a/src/screens/LoginScreen.tsx
+++ b/src/screens/LoginScreen.tsx
@@ -1,5 +1,5 @@
-import { Formik, FormikHelpers } from 'formik';
-import React from 'react';
+import { Formik, FormikHelpers, FormikProps } from 'formik';
+import React, { useEffect, useRef } from 'react';
import { View, Text, ScrollView, Image, Alert } from 'react-native';
import { Button } from '../components/Button';
import { Input } from '../components/Input';
@@ -13,8 +13,17 @@ import { RootStackParams } from '../navigators/Navigator';
import { useAppDispatch, useAppSelector } from '../hooks';
import { colors } from '../util/colors';
import { fonts } from '../util/fonts';
+import { isBiometricSupported } from '../util/biometric';
import { SingleSelectInput } from '../components/SingleSelectInput';
import { loginFlow } from '../store/thunks/account';
+import * as LocalAuthentication from 'expo-local-authentication';
+import { SignInStatus } from '../store/types/enums/SignInStatus';
+import {
+ ACCOUNT_AUTHENTICATION_KEY,
+ getStoredValue,
+ setStoreData,
+} from '../util/secureStore';
+import { selectBiometricUseStatus } from '../store/selectors/account';
const SYNC_EXISTING = 'sync_existing';
@@ -23,6 +32,13 @@ type LoginFormValues = {
password: string;
};
+export type StoredAuthenticationValues = {
+ [key: string]: {
+ password: string;
+ email: string;
+ };
+};
+
const LoginFormSchema = Yup.object().shape({
email: Yup.string().required('Required'),
password: Yup.string().required('Required'),
@@ -33,11 +49,45 @@ export type LoginScreenProps = NativeStackScreenProps;
export const LoginScreen = (props: LoginScreenProps) => {
const dispatch = useAppDispatch();
const headerHeight = useHeaderHeight();
+ const formRef = useRef>(null);
- const { localUsernames, lastUsername } = useAppSelector(
+ const biometricUseStatus = useAppSelector(selectBiometricUseStatus);
+ const { localUsernames, lastUsername, signInStatus } = useAppSelector(
state => state.account,
);
+ useEffect(() => {
+ (async () => {
+ if (signInStatus === SignInStatus.INITIAL) {
+ const isBiometricSupport = await isBiometricSupported();
+
+ if (
+ isBiometricSupport &&
+ lastUsername &&
+ biometricUseStatus?.[lastUsername]
+ ) {
+ await LocalAuthentication.authenticateAsync({
+ promptMessage:
+ 'To use Telios, you must have identity verification.',
+ cancelLabel: 'Cancel',
+ disableDeviceFallback: false,
+ }).then(async res => {
+ if (res?.success) {
+ const authenticationData: StoredAuthenticationValues =
+ await getStoredValue(ACCOUNT_AUTHENTICATION_KEY);
+ const lastUserAuthData = authenticationData?.[lastUsername || ''];
+ formRef?.current?.setFieldValue(
+ 'password',
+ lastUserAuthData.password,
+ );
+ formRef?.current?.handleSubmit?.();
+ }
+ });
+ }
+ }
+ })();
+ }, []);
+
const onSubmit = async (
values: LoginFormValues,
actions: FormikHelpers,
@@ -51,6 +101,14 @@ export const LoginScreen = (props: LoginScreenProps) => {
if (loginResponse.type === loginFlow.rejected.type) {
actions.setSubmitting(false);
Alert.alert('Login Failed', 'Invalid login credentials');
+ } else if (loginResponse.type === loginFlow.fulfilled.type) {
+ const authenticationData: StoredAuthenticationValues = {
+ [values.email]: {
+ email: values.email,
+ password: values.password,
+ },
+ };
+ setStoreData(ACCOUNT_AUTHENTICATION_KEY, authenticationData);
}
} catch (error) {
console.log('onSubmit error caught', error);
@@ -95,6 +153,7 @@ export const LoginScreen = (props: LoginScreenProps) => {
email: lastUsername || '',
password: '',
}}
+ innerRef={formRef}
validationSchema={LoginFormSchema}
onSubmit={onSubmit}>
{({
diff --git a/src/screens/Profile/ProfileScreen/constants.ts b/src/screens/Profile/ProfileScreen/constants.ts
index 773fdf9..893f3c7 100644
--- a/src/screens/Profile/ProfileScreen/constants.ts
+++ b/src/screens/Profile/ProfileScreen/constants.ts
@@ -43,6 +43,12 @@ export const menuItems: MenuItem[] = [
screenName: 'security',
icon: 'ios-shield-checkmark-outline',
},
+ {
+ label: 'Biometric Settings',
+ key: 'biometricSettings',
+ screenName: 'biometricSettings',
+ icon: 'ios-scan',
+ },
{
label: 'Log Out',
key: 'log-out',
diff --git a/src/screens/Profile/_ContactsScreen.tsx b/src/screens/Profile/_ContactsScreen.tsx
deleted file mode 100644
index 7922276..0000000
--- a/src/screens/Profile/_ContactsScreen.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { NativeStackScreenProps } from '@react-navigation/native-stack';
-import React from 'react';
-import { View, Text } from 'react-native';
-import { ProfileStackParams } from '../../navigators/Navigator';
-
-export type ContactsScreenProps = NativeStackScreenProps<
- ProfileStackParams,
- 'contacts'
->;
-
-export const ContactsScreen = ({}: ContactsScreenProps) => {
- return (
-
- Contacts Screen
-
- );
-};
diff --git a/src/screens/RegisterSuccessScreen.tsx b/src/screens/RegisterSuccessScreen.tsx
index d312fc5..767abe8 100644
--- a/src/screens/RegisterSuccessScreen.tsx
+++ b/src/screens/RegisterSuccessScreen.tsx
@@ -2,7 +2,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useHeaderHeight } from '@react-navigation/elements';
import * as Clipboard from 'expo-clipboard';
-import React from 'react';
+import React, { useState } from 'react';
import { View, Text, ScrollView } from 'react-native';
import { Button } from '../components/Button';
import { CoreStackProps, RegisterStackParams } from '../navigators/Navigator';
@@ -11,7 +11,8 @@ import { borderRadius, spacing } from '../util/spacing';
import { colors } from '../util/colors';
import { useAppDispatch, useAppSelector } from '../hooks';
import { CompositeScreenProps } from '@react-navigation/native';
-import { updateIsSignedIn } from '../store/thunks/account';
+import { updateSignInStatus } from '../store/thunks/account';
+import { SignInStatus } from '../store/types/enums/SignInStatus';
export type RegisterSuccessScreenProps = CompositeScreenProps<
NativeStackScreenProps,
@@ -31,7 +32,7 @@ export const RegisterSuccessScreen = (props: RegisterSuccessScreenProps) => {
};
const onDone = async () => {
- await dispatch(updateIsSignedIn(true));
+ await dispatch(updateSignInStatus(SignInStatus.FIRST_SIGNED_IN));
};
return (
diff --git a/src/store/account.ts b/src/store/account.ts
index 95a61f4..600f6f8 100644
--- a/src/store/account.ts
+++ b/src/store/account.ts
@@ -1,19 +1,21 @@
-import { createSlice } from '@reduxjs/toolkit';
+import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
accountLogin,
accountRetrieveStats,
accountUpdate,
getStoredUsernames,
registerNewAccount,
- updateIsSignedIn,
+ updateSignInStatus,
} from './thunks/account';
import { accountLogout } from './thunks/accountLogout';
import { LoginAccount, SignupAccount, Stats } from './types';
+import { SignInStatus } from './types/enums/SignInStatus';
interface AccountState {
localUsernames: string[];
+ biometricUseStatus: { [key: string]: boolean };
lastUsername?: string;
- isSignedIn: boolean;
+ signInStatus: SignInStatus;
signupAccount?: SignupAccount;
loginAccount?: LoginAccount;
isProfileUpdating?: boolean;
@@ -21,21 +23,30 @@ interface AccountState {
}
const initialState: AccountState = {
+ signInStatus: SignInStatus.INITIAL,
localUsernames: [],
- isSignedIn: false,
+ biometricUseStatus: {},
};
export const accountSlice = createSlice({
name: 'account',
initialState,
- reducers: {},
+ reducers: {
+ updateBiometricUseStatus: (
+ state,
+ action: PayloadAction<{ [key: string]: boolean }>,
+ ) => {
+ state.biometricUseStatus = action.payload;
+ },
+ },
extraReducers: builder => {
- builder.addCase(updateIsSignedIn, (state, action) => {
- state.isSignedIn = action.payload;
+ builder.addCase(updateSignInStatus, (state, action) => {
+ state.signInStatus = action.payload;
});
builder.addCase(getStoredUsernames.fulfilled, (state, action) => {
state.localUsernames = action.payload.usernames;
state.lastUsername = action.payload.lastUsername;
+ state.biometricUseStatus = action.payload.biometricUseStatus;
});
builder.addCase(registerNewAccount.fulfilled, (state, action) => {
state.signupAccount = action.payload;
@@ -48,6 +59,8 @@ export const accountSlice = createSlice({
const newState = { ...initialState };
newState.localUsernames = state.localUsernames;
newState.lastUsername = state.lastUsername;
+ newState.biometricUseStatus = state.biometricUseStatus;
+ newState.signInStatus = SignInStatus.SIGNED_OUT;
return newState;
});
@@ -72,3 +85,4 @@ export const accountSlice = createSlice({
});
export const accountReducer = accountSlice.reducer;
+export const { updateBiometricUseStatus } = accountSlice.actions;
diff --git a/src/store/selectors/account.ts b/src/store/selectors/account.ts
index 4253fe8..7411d12 100644
--- a/src/store/selectors/account.ts
+++ b/src/store/selectors/account.ts
@@ -1,7 +1,17 @@
import { RootState } from '../../store';
+import { SignInStatus } from '../types/enums/SignInStatus';
export const accountSelector = (state: RootState) => state.account;
-export const selectIsSignedIn = (state: RootState) => state.account.isSignedIn;
+
+export const selectIsSignedIn = (state: RootState) =>
+ state.account.signInStatus === SignInStatus.SIGNED_IN ||
+ state.account.signInStatus === SignInStatus.FIRST_SIGNED_IN;
+
+export const selectIsFirstSignIn = (state: RootState) =>
+ state.account.signInStatus === SignInStatus.FIRST_SIGNED_IN;
+
+export const selectSignInStatus = (state: RootState) =>
+ state.account.signInStatus;
export const loginAccountSelector = (state: RootState) =>
state.account.loginAccount;
@@ -18,3 +28,9 @@ export const selectAccountAvatar = (state: RootState) =>
export const selectAccountId = (state: RootState) =>
state.account?.loginAccount?.accountId;
+
+export const selectLastLoggedUsername = (state: RootState) =>
+ state.account?.lastUsername;
+
+export const selectBiometricUseStatus = (state: RootState) =>
+ state.account?.biometricUseStatus;
diff --git a/src/store/thunks/account.ts b/src/store/thunks/account.ts
index 4cde178..e4e9aa6 100644
--- a/src/store/thunks/account.ts
+++ b/src/store/thunks/account.ts
@@ -1,7 +1,9 @@
import { createAction, createAsyncThunk } from '@reduxjs/toolkit';
import {
+ getAsyncStorageBiometricUseStatus,
getAsyncStorageLastUsername,
getAsyncStorageSavedUsernames,
+ storeAsyncStorageBiometricUseStatus,
storeAsyncStorageLastUsername,
storeAsyncStorageSavedUsername,
} from '../../util/asyncStorage';
@@ -23,13 +25,24 @@ import {
SEND_RECOVERY_CODE,
} from '../types/events';
import nodejs from 'nodejs-mobile-react-native';
+import { SignInStatus } from '../types/enums/SignInStatus';
+import { StoredAuthenticationValues } from '../../screens/LoginScreen';
+import {
+ ACCOUNT_AUTHENTICATION_KEY,
+ setStoreData,
+} from '../../util/secureStore';
export const getStoredUsernames = createAsyncThunk(
'system/getStoredUsernames',
- async (): Promise<{ usernames: string[]; lastUsername?: string }> => {
+ async (): Promise<{
+ usernames: string[];
+ lastUsername?: string;
+ biometricUseStatus: { [key: string]: boolean };
+ }> => {
const usernames = await getAsyncStorageSavedUsernames();
const lastUsername = await getAsyncStorageLastUsername();
- return { usernames, lastUsername };
+ const biometricUseStatus = await getAsyncStorageBiometricUseStatus();
+ return { usernames, lastUsername, biometricUseStatus };
},
);
@@ -82,6 +95,7 @@ export const registerFlow = createAsyncThunk(
await storeAsyncStorageSavedUsername(data.email);
await storeAsyncStorageLastUsername(data.email);
+ await storeAsyncStorageBiometricUseStatus(data.email, false);
thunkAPI.dispatch(getStoredUsernames()); // that is needed to update redux latestAccount and localUsernames, needs refactor.
// Hint: use redux persistent slice
@@ -94,6 +108,14 @@ export const registerFlow = createAsyncThunk(
// getNewMailFlow is non-blocking
thunkAPI.dispatch(getNewMailFlow());
+
+ const authenticationData: StoredAuthenticationValues = {
+ [data.email]: {
+ email: data.email,
+ password: data.masterPassword,
+ },
+ };
+ setStoreData(ACCOUNT_AUTHENTICATION_KEY, authenticationData);
},
);
@@ -119,7 +141,7 @@ export const loginFlow = createAsyncThunk(
if (loginResponse.type === accountLogin.rejected.type) {
throw new Error(JSON.stringify(loginResponse.payload));
}
- thunkAPI.dispatch(updateIsSignedIn(true));
+ thunkAPI.dispatch(updateSignInStatus(SignInStatus.SIGNED_IN));
initMessageListener();
@@ -213,8 +235,8 @@ export type CreateAccountSyncInfoResponse = {
email: string;
};
-export const updateIsSignedIn = createAction(
- 'local/account:isSignedIn',
+export const updateSignInStatus = createAction(
+ 'local/account:updateSignInStatus',
);
export const getAccountSyncInfo = createNodeCalloutAsyncThunk<
diff --git a/src/store/types/enums/SignInStatus.ts b/src/store/types/enums/SignInStatus.ts
new file mode 100644
index 0000000..56e321d
--- /dev/null
+++ b/src/store/types/enums/SignInStatus.ts
@@ -0,0 +1,6 @@
+export enum SignInStatus {
+ INITIAL = 'initial',
+ SIGNED_IN = 'signedIn',
+ FIRST_SIGNED_IN = 'firstSignedIn',
+ SIGNED_OUT = 'signedOut',
+}
diff --git a/src/util/asyncStorage.ts b/src/util/asyncStorage.ts
index d2629dc..d33ce3a 100644
--- a/src/util/asyncStorage.ts
+++ b/src/util/asyncStorage.ts
@@ -3,6 +3,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
export const storage = {
savedUsernames: '@saved_usernames_json',
lastUsername: '@last_used_username_string',
+ biometricUseStatus: '@biometric_use_status',
};
export const getAsyncStorageSavedUsernames = async () => {
@@ -47,3 +48,34 @@ export const getAsyncStorageLastUsername = async () => {
export const storeAsyncStorageLastUsername = async (email: string) => {
await AsyncStorage.setItem(storage.lastUsername, email);
};
+
+export const getAsyncStorageBiometricUseStatus = async (): Promise<{
+ [key: string]: boolean;
+}> => {
+ try {
+ const jsonValue = await AsyncStorage.getItem(storage.biometricUseStatus);
+ const biometricUseStatus =
+ jsonValue != null
+ ? (JSON.parse(jsonValue) as {
+ [key: string]: boolean;
+ })
+ : {};
+ return biometricUseStatus;
+ } catch (e) {
+ // error reading value
+ console.log('ERROR GETTING BIOMETRIC USE STATUS: ', e);
+ return {};
+ }
+};
+
+export const storeAsyncStorageBiometricUseStatus = async (
+ email: string,
+ usingStatus: boolean,
+) => {
+ const biometricUseStatus: {
+ [key: string]: boolean;
+ } = await getAsyncStorageBiometricUseStatus();
+ biometricUseStatus[email] = usingStatus;
+ const jsonValue = JSON.stringify(biometricUseStatus);
+ await AsyncStorage.setItem(storage.biometricUseStatus, jsonValue);
+};
diff --git a/src/util/biometric.ts b/src/util/biometric.ts
new file mode 100644
index 0000000..41b8c3b
--- /dev/null
+++ b/src/util/biometric.ts
@@ -0,0 +1,7 @@
+import * as LocalAuthentication from 'expo-local-authentication';
+
+export const isBiometricSupported = async (): Promise => {
+ const compatible = await LocalAuthentication.hasHardwareAsync();
+ const savedBiometrics = await LocalAuthentication.isEnrolledAsync();
+ return compatible && savedBiometrics;
+};
diff --git a/src/util/secureStore.ts b/src/util/secureStore.ts
new file mode 100644
index 0000000..73ce8de
--- /dev/null
+++ b/src/util/secureStore.ts
@@ -0,0 +1,21 @@
+import * as SecureStore from 'expo-secure-store';
+
+export function setStoreData(
+ key: string,
+ value: any,
+ options?: SecureStore.SecureStoreOptions,
+) {
+ const preparedValue: string =
+ typeof value === 'string' ? value : JSON.stringify(value);
+ const storeOptions = options || {
+ keychainAccessible: SecureStore.WHEN_UNLOCKED,
+ };
+ SecureStore.setItemAsync(key, preparedValue, storeOptions);
+}
+
+export async function getStoredValue(key: string) {
+ let result = await SecureStore.getItemAsync(key);
+ return result && JSON.parse(result);
+}
+
+export const ACCOUNT_AUTHENTICATION_KEY = 'ACCOUNT_AUTHENTICATION_KEY';
diff --git a/yarn.lock b/yarn.lock
index e9f7ff7..17ef95e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4752,6 +4752,14 @@ expo-keep-awake@~10.0.2:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-10.0.2.tgz#706bda839782bb3e8ad4cbe43bde471a56368813"
integrity sha512-Ro1lgyKldbFs4mxhWM+goX9sg0S2SRR8FiJJeOvaRzf8xNhrZfWA00Zpr+/3ocCoWQ3eEL+X9UF4PXXHf0KoOg==
+expo-local-authentication@~12.1.0:
+ version "12.1.1"
+ resolved "https://registry.yarnpkg.com/expo-local-authentication/-/expo-local-authentication-12.1.1.tgz#fceaa6c2f3a5aee8b5d004992e13ff5b6c279284"
+ integrity sha512-G2BqzteR3Ip9Qs57dD6vUFsqJYLz5xCBGAi1dd2DudVNuw38Scx1Bq0Y2VCoFsDuD+6WUlNjL4+oUQlWZm7Xug==
+ dependencies:
+ "@expo/config-plugins" "^4.0.2"
+ invariant "^2.2.4"
+
expo-modules-autolinking@0.5.5, expo-modules-autolinking@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-0.5.5.tgz#6bcc42072dcbdfca79d207b7f549f1fdb54a2b74"
@@ -4771,6 +4779,11 @@ expo-modules-core@0.6.5:
compare-versions "^3.4.0"
invariant "^2.2.4"
+expo-secure-store@~11.1.0:
+ version "11.1.1"
+ resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-11.1.1.tgz#16f727f56f5b0eef9817fc0d64d79be26f8a34a8"
+ integrity sha512-pCajYYoZbl8IORxXw83SLUsWlU6a+HmjJZP8oiSvkG1KnZ5gPepiAMRRmcFUApYXBgiOvKRGOBEUfVwmR7yO4A==
+
expo-splash-screen@~0.14.0:
version "0.14.2"
resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.14.2.tgz#2598d6980e71ecd8b7467ca821fb9dbfb80f355b"