Skip to content

Commit

Permalink
Add Unlock screen design
Browse files Browse the repository at this point in the history
  • Loading branch information
HeesungB committed Dec 10, 2023
1 parent c94f9ef commit 9092c43
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 23 deletions.
1 change: 1 addition & 0 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@walletconnect/sign-client": "^2.10.5",
"buffer": "^6.0.3",
"color": "^4.2.3",
"delay": "^6.0.0",
"expo": "^49.0.6",
"expo-clipboard": "~4.3.1",
"expo-crypto": "~12.4.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/src/components/drawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const DrawerContent: FunctionComponent = observer(() => {
const handleLock = () => {
keyRingStore.lock();
drawerClose();
navigation.reset({routes: [{name: 'Locked'}]});
navigation.reset({routes: [{name: 'Unlock'}]});
};

const onClickManageChains = () => {
Expand Down
49 changes: 35 additions & 14 deletions packages/mobile/src/components/text-button/text-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@ import React, {
useState,
} from 'react';
import {useStyle} from '../../styles';
import {Text, StyleSheet, TextStyle, Pressable, ViewStyle} from 'react-native';
import {
Text,
StyleSheet,
TextStyle,
Pressable,
ViewStyle,
View,
} from 'react-native';
import {Box} from '../box';
import {SVGLoadingIcon} from '../spinner';

type ButtonColorType = 'faint' | 'default';
export type ButtonSize = 'small' | 'large';
Expand All @@ -17,6 +25,7 @@ export const TextButton: FunctionComponent<{
text: string;
rightIcon?: ReactElement | ((color: string) => ReactElement);
disabled?: boolean;
loading?: boolean;
onPress?: () => void;
//NOTE textStyle에서 색상을 변경하면 pressing 컬러가 먹히지 않기 떄문에 Omit으로 color만 제거함
textStyle?: Omit<TextStyle, 'color'>;
Expand All @@ -30,6 +39,7 @@ export const TextButton: FunctionComponent<{
text,
rightIcon,
disabled = false,
loading,
onPress,
textStyle,
containerStyle,
Expand Down Expand Up @@ -79,19 +89,30 @@ export const TextButton: FunctionComponent<{
onPress={onPress}
onPressOut={() => setIsPressIn(false)}
onPressIn={() => setIsPressIn(true)}>
<Text
style={StyleSheet.flatten([
style.flatten([
'text-center',
size === 'large' ? 'text-button1' : 'text-button2',
]),
{
color: isPressIn ? pressingColorDefinition : textColorDefinition,
},
textStyle,
])}>
{text}
</Text>
{loading ? (
<View
style={style.flatten([
'absolute-fill',
'justify-center',
'items-center',
])}>
<SVGLoadingIcon color={style.get('color-blue-400').color} size={16} />
</View>
) : (
<Text
style={StyleSheet.flatten([
style.flatten([
'text-center',
size === 'large' ? 'text-button1' : 'text-button2',
]),
{
color: isPressIn ? pressingColorDefinition : textColorDefinition,
},
textStyle,
])}>
{text}
</Text>
)}
<Box height={1} marginLeft={4} alignY="center">
{isValidElement(rightIcon) ||
!rightIcon ||
Expand Down
14 changes: 6 additions & 8 deletions packages/mobile/src/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
TransitionPresets,
} from '@react-navigation/stack';
import {HomeScreen} from './screen/home';
import {LockedScreen} from './screen/locked';
import {UnlockScreen} from './screen/unlock';
import {SendSelectAssetScreen} from './screen/send/select-asset';
import {createDrawerNavigator, useDrawerStatus} from '@react-navigation/drawer';
import {DrawerContent} from './components/drawer';
Expand Down Expand Up @@ -218,7 +218,7 @@ export type RootStackParamList = {
| {chainId?: string; contractAddress?: string}
| undefined;

Locked: undefined;
Unlock: undefined;
SelectWallet: undefined;
'SelectWallet.Intro': undefined;
'SelectWallet.Delete': {id: string};
Expand Down Expand Up @@ -833,7 +833,7 @@ export const AppNavigation: FunctionComponent = observer(() => {
initialRouteName={(() => {
switch (keyRingStore.status) {
case 'locked':
return 'Locked';
return 'Unlock';
case 'unlocked':
return 'Home';
case 'empty':
Expand All @@ -851,11 +851,9 @@ export const AppNavigation: FunctionComponent = observer(() => {
component={MainTabNavigationWithDrawer}
/>
<Stack.Screen
options={{
...defaultHeaderOptions,
}}
name="Locked"
component={LockedScreen}
options={{headerShown: false}}
name="Unlock"
component={UnlockScreen}
/>
<Stack.Screen
name="Register"
Expand Down
213 changes: 213 additions & 0 deletions packages/mobile/src/screen/unlock/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import React, {
FunctionComponent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {observer} from 'mobx-react-lite';
import {useStore} from '../../stores';
import {FormattedMessage, useIntl} from 'react-intl';
import {useStyle} from '../../styles';
import {useNavigation} from '@react-navigation/native';
import {WalletStatus} from '@keplr-wallet/stores';
import {autorun} from 'mobx';
import {StackNavProp} from '../../navigation';
import {PageWithScrollView} from '../../components/page';
import {Text} from 'react-native';
import {Box} from '../../components/box';
import LottieView from 'lottie-react-native';
import {Gutter} from '../../components/gutter';
import {TextInput} from '../../components/input';
import {Button} from '../../components/button';
import {TextButton} from '../../components/text-button';
import delay from 'delay';

export const UnlockScreen: FunctionComponent = observer(() => {
const {keyRingStore, keychainStore, accountStore, chainStore} = useStore();

const intl = useIntl();
const style = useStyle();
const navigation = useNavigation<StackNavProp>();

const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isBiometricLoading, setIsBiometricLoading] = useState(false);
const [error, setError] = useState<Error | undefined>();

const tryBiometricAutoOnce = useRef(false);

const waitAccountInit = useCallback(async () => {
if (keyRingStore.status === 'unlocked') {
for (const chainInfo of chainStore.chainInfos) {
const account = accountStore.getAccount(chainInfo.chainId);
if (account.walletStatus === WalletStatus.NotInit) {
account.init();
}
}

await new Promise<void>(resolve => {
const disposal = autorun(() => {
// account init은 동시에 발생했을때 debounce가 되므로
// 첫번째꺼 하나만 확인해도 된다.
if (
accountStore.getAccount(chainStore.chainInfos[0].chainId)
.bech32Address
) {
resolve();
if (disposal) {
disposal();
}
}
});
});
}
}, [accountStore, chainStore, keyRingStore]);

const tryBiometric = useCallback(async () => {
try {
setIsBiometricLoading(true);

// Because javascript is synchronous language, the loadnig state change would not delivered to the UI thread
// So to make sure that the loading state changes, just wait very short time.
await delay(10);
await keychainStore.tryUnlockWithBiometry();
await waitAccountInit();
navigation.replace('Home');
} catch (e) {
console.log(e);
} finally {
setIsBiometricLoading(false);
}
}, [keychainStore, navigation, waitAccountInit]);

const tryUnlock = async () => {
try {
setIsLoading(true);

// Decryption needs slightly huge computation.
// Because javascript is synchronous language, the loadnig state change would not delivered to the UI thread
// before the actually decryption is complete.
// So to make sure that the loading state changes, just wait very short time.
await delay(10);
await keyRingStore.unlock(password);
await waitAccountInit();
navigation.replace('Home');
} catch (e) {
console.log(e);

setIsLoading(false);
setError(e.message);
}
};

//For a one-time biometric authentication
useEffect(() => {
if (
!tryBiometricAutoOnce.current &&
keychainStore.isBiometryOn &&
keyRingStore.status === 'locked'
) {
tryBiometricAutoOnce.current = true;
(async () => {
try {
setIsBiometricLoading(true);
// Because javascript is synchronous language, the loadnig state change would not delivered to the UI thread
// So to make sure that the loading state changes, just wait very short time.
await delay(10);

await keychainStore.tryUnlockWithBiometry();
await waitAccountInit();

navigation.replace('Home');
} catch (e) {
console.log(e);
} finally {
setIsBiometricLoading(false);
}
})();
}
}, [
keyRingStore.status,
keychainStore,
keychainStore.isBiometryOn,
navigation,
waitAccountInit,
]);

return (
<PageWithScrollView
backgroundMode={'default'}
contentContainerStyle={style.get('flex-grow-1')}
style={style.flatten(['padding-x-24'])}>
<Box style={{flex: 1}} alignX="center" alignY="center">
<Box style={{flex: 1}} />

<LottieView
source={require('../../public/assets/lottie/wallet/logo.json')}
style={{width: 200, height: 155}}
/>

<Text style={style.flatten(['h1', 'color-text-high'])}>
<FormattedMessage id="page.unlock.paragraph-section.welcome-back" />
</Text>

<Gutter size={70} />

<TextInput
label={intl.formatMessage({
id: 'page.unlock.bottom-section.password-input-label',
})}
value={password}
containerStyle={{width: '100%'}}
secureTextEntry={true}
returnKeyType="done"
onChangeText={setPassword}
onSubmitEditing={async () => {
await tryUnlock();
}}
error={
error
? intl.formatMessage({id: 'error.invalid-password'})
: undefined
}
/>

<Gutter size={34} />

<Button
text={intl.formatMessage({id: 'page.unlock.unlock-button'})}
size="large"
onPress={tryUnlock}
loading={isLoading}
containerStyle={{width: '100%'}}
/>

<Gutter size={32} />

{keychainStore.isBiometryOn ? (
<TextButton
text="Use Biometric Authentication"
size="large"
loading={isBiometricLoading}
onPress={async () => {
await tryBiometric();
}}
/>
) : null}

<Box style={{flex: 1}} />

<TextButton
color="faint"
text={intl.formatMessage({
id: 'page.unlock.forgot-password-button',
})}
size="large"
/>

<Gutter size={32} />
</Box>
</PageWithScrollView>
);
});
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7711,6 +7711,7 @@ __metadata:
browserify: ^17.0.0
buffer: ^6.0.3
color: ^4.2.3
delay: ^6.0.0
expo: ^49.0.6
expo-clipboard: ~4.3.1
expo-crypto: ~12.4.1
Expand Down Expand Up @@ -19487,6 +19488,13 @@ __metadata:
languageName: node
linkType: hard

"delay@npm:^6.0.0":
version: 6.0.0
resolution: "delay@npm:6.0.0"
checksum: e00190cf6e56e3f746af6664a9b7a837a582a70b96ce18d83b86a97300cc9f727189b9f6a7082557134223c0bd23eee88e681cab54cb4e5d8f6b2f4054e7b49a
languageName: node
linkType: hard

"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
Expand Down

0 comments on commit 9092c43

Please sign in to comment.