Skip to content

Commit

Permalink
feat(app): UI for keychain creation (#94)
Browse files Browse the repository at this point in the history
#### What this PR does / why we need it:

As per title, UI only, still storyboarding.
  • Loading branch information
fuxingloh committed Jul 17, 2023
1 parent 2ee92cf commit f7db5a7
Show file tree
Hide file tree
Showing 17 changed files with 769 additions and 22 deletions.
2 changes: 2 additions & 0 deletions app/app/HapticFeedback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface HapticFeedbackOptions {
impactAsync(style?: ImpactFeedbackStyle): Promise<void>;
}

export { ImpactFeedbackStyle, NotificationFeedbackType };

const HapticFeedbackContext = createContext<HapticFeedbackOptions>({} as any);

/**
Expand Down
4 changes: 4 additions & 0 deletions app/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ function AppContainer(): ReactElement | null {
>
<Stack.Screen name="index" redirect />
<Stack.Screen name="tabs" options={{ headerShown: false }} />

{/* For key related screens, they're set to full screen modal to force closing of modal to be a more deliberate action. */}
<Stack.Screen name="keys/setup" options={{ headerShown: false, presentation: 'fullScreenModal' }} />
<Stack.Screen name="keys/settings" options={{ headerShown: false, presentation: 'fullScreenModal' }} />
</Stack>
</ExternalLinkProvider>
</HapticFeedbackProvider>
Expand Down
10 changes: 9 additions & 1 deletion app/app/about/design.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export default function DesignSystemPage(): ReactElement {
<Text style={tailwind('text-sm text-zinc-200 font-mono')}>Font-Mono</Text>
<Code>text-sm text-zinc-200 font-mono</Code>
</View>
<View style={tailwind('mt-2')}>
<Text style={tailwind('text-base font-bold text-red-600')}>Error, something went wrong.</Text>
<Code>text-base font-bold text-red-600</Code>
</View>
<View style={tailwind('mt-2')}>
<Text style={tailwind('text-base font-bold text-green-600')}>Success!</Text>
<Code>text-base font-bold text-green-600</Code>
</View>
</Section>

<Section title="ICONS">
Expand Down Expand Up @@ -181,7 +189,7 @@ export default function DesignSystemPage(): ReactElement {
<View style={tailwind('bg-zinc-950 opacity-60 w-full h-px')} />
</View>
<ListViewItem title="Item 2" subtitle="" icon="infocirlceo" />
<Text style={tailwind('pt-8 pb-2 px-6 text-white bg-zinc-950')}>SECTION</Text>
<Text style={tailwind('pt-8 pb-2 px-6 text-zinc-300 bg-zinc-950')}>SECTION</Text>
<ListViewItem title="Item 3" subtitle="" icon="retweet" />
</View>
);
Expand Down
14 changes: 0 additions & 14 deletions app/app/keys/settings.tsx

This file was deleted.

27 changes: 27 additions & 0 deletions app/app/keys/settings/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Stack, useRouter } from 'expo-router';
import { ReactElement } from 'react';
import { useTailwind } from 'tailwind-rn';

import { StackHeaderClose } from '../../../components/StackHeader';

export default function KeySettingLayout(): ReactElement {
const tailwind = useTailwind();
const router = useRouter();

return (
<Stack
screenOptions={{
headerShown: true,
headerStyle: tailwind('bg-zinc-900'),
headerLeft: () => null,
headerRight: () => (
<StackHeaderClose
onPress={() => {
router.push('/tabs/settings');
}}
/>
),
}}
/>
);
}
153 changes: 153 additions & 0 deletions app/app/keys/settings/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Stack, useRouter } from 'expo-router';
import { PropsWithChildren, ReactElement } from 'react';
import { SafeAreaView, ScrollView, Switch, Text, TouchableOpacity, View } from 'react-native';
import { useTailwind } from 'tailwind-rn';

import { NotificationFeedbackType, useHaptic } from '../../HapticFeedback';
import { IconSet, IconSetName } from '../../IconSet';

export default function KeySettingsPage(): ReactElement {
const tailwind = useTailwind();
// TODO(fuxingloh): setting to toggle show your mnemonic

return (
<>
<Stack.Screen
options={{
title: 'Keychain Settings',
}}
/>
<SafeAreaView style={tailwind('flex-1 bg-zinc-950')}>
<ScrollView contentContainerStyle={tailwind('py-6')}>
<Text style={tailwind('py-3 px-6 text-zinc-300 bg-zinc-950')}>SECURITY</Text>
<KeychainSettingRowPasscode />
<Text style={tailwind('py-3 px-6 text-zinc-300 bg-zinc-950')}>BIP32 & BIP39</Text>
<KeychainSettingRowBip32Scheme />
<KeychainSettingRowMaxLength />
<KeychainSettingRowBip32Hardened />
<KeychainSettingRowBip39Language />
</ScrollView>
</SafeAreaView>
</>
);
}

export function KeychainSettingRowBip32Scheme(): ReactElement {
const tailwind = useTailwind();

return (
<KeychainSettingRow title="BIP32 Derivation Method" icon="bars">
<Text style={tailwind('text-base text-zinc-200 opacity-60')}>m/0'/0'/0'/0'/i'</Text>
</KeychainSettingRow>
);
}

export function KeychainSettingRowBip32Hardened(): ReactElement {
const tailwind = useTailwind();
const haptic = useHaptic();

return (
<KeychainSettingRow
title="BIP32 Hardened"
icon="Safety"
description="Limit the keychain to hardened derivation paths. This enhances the security of your keychain by preventing the use of child keys to derive the parent key."
onPress={() => haptic.notificationAsync(NotificationFeedbackType.Error)}
>
<Switch
disabled
value
thumbColor={tailwind('text-zinc-200').color as any}
trackColor={{
false: tailwind('text-teal-800').color as any,
true: tailwind('text-teal-800').color as any,
}}
/>
</KeychainSettingRow>
);
}

export function KeychainSettingRowBip39Language(): ReactElement {
const tailwind = useTailwind();

return (
<KeychainSettingRow
title="BIP39 Language"
icon="infocirlceo"
description="To maintain compatibility of your keychain with other wallets, we only permit the use of the default English wordlist."
>
<Text style={tailwind('text-base text-zinc-200 opacity-60')}>English</Text>
</KeychainSettingRow>
);
}

export function KeychainSettingRowMaxLength(): ReactElement {
const tailwind = useTailwind();

return (
<KeychainSettingRow title="BIP32 Maximum Derivation" icon="pushpino">
<Text style={tailwind('text-base text-zinc-200 opacity-60')}>1,000 Keys</Text>
</KeychainSettingRow>
);
}

export function KeychainSettingRowPasscode(): ReactElement {
const tailwind = useTailwind();
const haptic = useHaptic();
const router = useRouter();

return (
<KeychainSettingRow
title="Keychain Passcode"
icon="lock1"
description="This should not be confused with the BIP39 passphrase. This passcode is utilized to decrypt the keychain from the device's secure keystore once the app has been unlocked."
onPress={async () => {
await haptic.selectionAsync();
await router.push('keys/settings/passcode');
}}
>
<View style={tailwind('flex-row')}>
{[0, 1, 2, 3, 4, 5].map((index) => (
<View key={index} style={tailwind('w-1.5 h-1.5 bg-zinc-200 rounded-full ml-1')} />
))}
</View>
</KeychainSettingRow>
);
}

function KeychainSettingRow(
props: PropsWithChildren<{
title: string;
icon: IconSetName;
description?: string;
onPress?: () => void;
divider?: boolean;
}>,
): ReactElement {
const tailwind = useTailwind();
return (
<View>
<TouchableOpacity
disabled={!props.onPress}
onPress={props.onPress}
style={tailwind('px-6 bg-zinc-900 flex-row items-center justify-between')}
>
<View style={tailwind('flex-row py-3 items-center justify-between')}>
<IconSet name={props.icon} size={20} style={tailwind('text-white')}></IconSet>
<Text style={tailwind('text-white text-base ml-2')}>{props.title}</Text>
</View>
<View>{props.children}</View>
</TouchableOpacity>
{props.description ? (
<View style={tailwind('px-6 pt-3 pb-6 bg-zinc-900/20')}>
<Text style={tailwind('text-sm text-zinc-400')}>{props.description}</Text>
</View>
) : (
(props.divider ?? true) && (
<View style={tailwind('pl-12 bg-zinc-900')}>
<View style={tailwind('bg-zinc-950 opacity-60 w-full h-px')} />
</View>
)
)}
</View>
);
}
63 changes: 63 additions & 0 deletions app/app/keys/settings/passcode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Stack } from 'expo-router';
import { ReactElement, useState } from 'react';
import { Keyboard, SafeAreaView, ScrollView, Text, View } from 'react-native';
import { useTailwind } from 'tailwind-rn';

import { PasscodeInput } from '../../../components/PasscodeInput';
import { StackHeaderBack } from '../../../components/StackHeader';

export default function SettingPasscodePage(): ReactElement {
const tailwind = useTailwind();
const [validated, setValidated] = useState(false);

return (
<>
<Stack.Screen
options={{
title: 'Keychain Passcode Settings',
headerLeft: () => <StackHeaderBack />,
}}
/>

<SafeAreaView style={tailwind('flex-1 bg-zinc-950')}>
<ScrollView contentContainerStyle={tailwind('p-6 flex-grow justify-between')}>
{validated ? (
<View style={tailwind('py-12')}>
<Text style={tailwind('text-2xl font-bold text-zinc-200')}>Change Passcode</Text>
</View>
) : (
<View style={tailwind('py-12')}>
<EnterPasscodeValidation
onValidated={() => {
setValidated(true);
}}
onError={() => {}}
/>
</View>
)}
</ScrollView>
</SafeAreaView>
</>
);

function EnterPasscodeValidation(props: { onValidated: () => void; onError: () => void }): ReactElement {
const [passcode, setPasscode] = useState('');

return (
<View>
<Text style={tailwind('text-2xl font-bold text-zinc-200 text-center')}>Enter Keychain Passcode</Text>

<PasscodeInput
value={passcode}
onValueChange={(text) => {
setPasscode(text);
if (text.length === 6) {
props.onValidated();
Keyboard.dismiss();
}
}}
/>
</View>
);
}
}
26 changes: 26 additions & 0 deletions app/app/keys/setup/_layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Stack, useRouter } from 'expo-router';
import { ReactElement } from 'react';
import { useTailwind } from 'tailwind-rn';

import { StackHeaderClose } from '../../../components/StackHeader';

export default function KeySetupLayout(): ReactElement {
const tailwind = useTailwind();
const router = useRouter();

return (
<Stack
screenOptions={{
headerShown: true,
headerStyle: tailwind('bg-zinc-900'),
headerRight: () => (
<StackHeaderClose
onPress={() => {
router.push('/');
}}
/>
),
}}
/>
);
}
50 changes: 50 additions & 0 deletions app/app/keys/setup/confirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { router, Stack } from 'expo-router';
import { ReactElement, useState } from 'react';
import { SafeAreaView, ScrollView, Text, TextInput, View } from 'react-native';
import { useTailwind } from 'tailwind-rn';

import { PrimaryActionButton } from '../../../components/Button';
import { StackHeaderBack } from '../../../components/StackHeader';

export default function SetupConfirmPage(): ReactElement {
const tailwind = useTailwind();
const [sentence, setSentence] = useState('');

return (
<>
<Stack.Screen
options={{
title: 'Confirm Keychain',
headerLeft: () => <StackHeaderBack />,
}}
/>

<SafeAreaView style={tailwind('flex-1 bg-zinc-950')}>
<ScrollView contentContainerStyle={tailwind('py-6 flex-grow justify-between')}>
<View>
<Text style={tailwind('text-lg font-bold text-zinc-200 mb-2 mx-6')}>Keychain Mnemonic (BIP39)</Text>
<TextInput
editable
multiline
onChangeText={(text) => setSentence(text)}
placeholder="Enter your mnemonic phrase here for verification."
value={sentence}
placeholderTextColor={tailwind('text-zinc-400').color as any}
style={tailwind('px-6 py-2 text-base text-zinc-200 bg-zinc-900 h-48')}
/>
</View>

<View style={tailwind('mx-6')}>
<PrimaryActionButton
onPress={async () => {
await router.push('keys/setup/passcode');
}}
>
Continue
</PrimaryActionButton>
</View>
</ScrollView>
</SafeAreaView>
</>
);
}
Loading

0 comments on commit f7db5a7

Please sign in to comment.