Skip to content

Commit

Permalink
Merge pull request #54 from Superlight-Labs/53-improve-onboarding-exp…
Browse files Browse the repository at this point in the history
…erience

feat(app): improved onboarding experience (#53)
  • Loading branch information
lauhon authored Jul 6, 2023
2 parents 251e2b2 + 347a285 commit 329cda4
Show file tree
Hide file tree
Showing 20 changed files with 242 additions and 106 deletions.
4 changes: 4 additions & 0 deletions apps/api/src/routes/register-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { registerTransactionRoutes } from './transaction.routes';
import { registerUserRoutes } from './user.routes';

export const registerRoutes = (server: FastifyInstance): void => {
server.get('/health', (_, res) => {
res.send({ status: 'ok' });
});

registerUserRoutes(server);
registerMcpRoutes(server);
registerAuthRoutes(server);
Expand Down
20 changes: 1 addition & 19 deletions apps/mobile/ios/Superlight/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,9 @@
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
<false />
<key>NSExceptionDomains</key>
<dict>
<key>superlight-api-test.westeurope.azurecontainer.io:3000</key>
<dict>
<key>NSIncludesSubdomains</key>
<true />
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true />
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
<key> superlight-api-test.westeurope.azurecontainer.io </key>
<dict>
<key>NSIncludesSubdomains</key>
<true />
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true />
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
</dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export type IconName =
| 'Info'
| 'AlertCircle'
| 'Clipboard'
| 'ExternalLink'
| 'Delete'
| 'ClipboardCopy'
| 'ArrowDownCircle'
| 'Lock'
| 'UserCheck'
Expand All @@ -66,6 +68,7 @@ export type IconName =
| 'Copy'
| 'Send'
| 'Minimize2'
| 'ChevronDown'
| 'CheckCircle'
| 'AtSign'
| 'Loading'
Expand Down
25 changes: 20 additions & 5 deletions apps/mobile/src/components/shared/snackbar/snackbar.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef } from 'react';
import { Animated, Easing } from 'react-native';
import { AppMessage, useSnackbarState } from 'state/snackbar.state';
import { createBugUrl, openWebsite } from 'utils/web-opener';
import { AnimatedView, Pressable, Text, View } from 'utils/wrappers/styled-react-native';
import MonoIcon, { IconName } from '../mono-icon/mono-icon.component';

Expand Down Expand Up @@ -34,26 +35,29 @@ const Snackbar = ({ appMessage }: Props) => {
return (
<AnimatedView
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
backgroundColor: 'white',
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bottom: 50,
left: 10,
right: 10,
transform: [
{ translateY: introAnim.interpolate({ inputRange: [0, 1], outputRange: [500, 0] }) },
],
}}>
<Pressable
onPress={close}
className={`flex rounded-2xl bg-${colors[level]}-100 border-2 border-[${font[level]}] border-[${font[level]}] py-4`}>
className={`flex w-full flex-col rounded-2xl bg-${colors[level]}-100 border-2 border-[${font[level]}] border-[${font[level]}] py-4`}>
<View className="flex flex-row items-center justify-around p-2 px-8">
{level === 'progress' ? (
<Text
className={`rounded-full px-2 py-1 font-manrope-bold text-xs text-[${font[level]}] font-manrope-bold uppercase`}>
{appMessage.step + '/' + appMessage.total}
</Text>
) : (
<MonoIcon color={font[level]} iconName={icons[level] as IconName} />
<MonoIcon style="w-16" color={font[level]} iconName={icons[level] as IconName} />
)}
<Text className={`text-[${font[level]}]`}>{message}</Text>
{level === 'progress' ? (
Expand All @@ -62,6 +66,17 @@ const Snackbar = ({ appMessage }: Props) => {
<MonoIcon style="opacity-0" iconName="Loading" />
)}
</View>
{level === 'error' && (
<Pressable
onPress={() => openWebsite(createBugUrl(appMessage.error))}
className="mt-4 flex flex-row items-center justify-center underline">
<Text className="mr-2 text-center underline">Report a Bug</Text>
<MonoIcon iconName="Github" />
</Pressable>
)}
</Pressable>
<Pressable className="mt-2" onPress={close}>
<MonoIcon iconName="ChevronDown" />
</Pressable>
</AnimatedView>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Clipboard from '@react-native-clipboard/clipboard';
import MonoIcon from 'components/shared/mono-icon/mono-icon.component';
import { useSnackbarState } from 'state/snackbar.state';
import { Pressable, Text, View } from 'utils/wrappers/styled-react-native';

type Props = {
phrase: string;
};

const RecoveryPhraseDisplayComponent = ({ phrase }: Props) => {
const words = phrase.split(' ');
const { setMessage } = useSnackbarState();

const copyToClipboard = () => {
Clipboard.setString(phrase);
setMessage({
level: 'info',
message:
'Copied your phrase to clipboard! Please consider writing it down on a piece of paper for safety!',
});
};

return (
<View className=" flex h-full w-full">
<View className="flex w-full flex-row items-center py-2">
<View className="flex-1">
<Text className="font-inter-medium">Recovery phrase</Text>
</View>
<View className="flex h-12 w-12 items-center justify-center rounded-lg bg-black p-3">
<MonoIcon color="white" iconName="ListRestart" />
</View>
</View>
<View className="mt-8 flex w-full flex-row justify-around">
<View className="flex w-[25vw] flex-col">
<Text className="font-inter-medium">1. {words.at(0)}</Text>
<Text className="font-inter-medium">2. {words.at(1)}</Text>
<Text className="font-inter-medium">3. {words.at(2)}</Text>
<Text className="font-inter-medium">4. {words.at(3)}</Text>
<Text className="font-inter-medium">5. {words.at(4)}</Text>
<Text className="font-inter-medium">6. {words.at(5)}</Text>
</View>
<View className="flex w-[25vw] flex-col">
<Text className="font-inter-medium">7. {words.at(6)}</Text>
<Text className="font-inter-medium">8. {words.at(7)}</Text>
<Text className="font-inter-medium">9. {words.at(8)}</Text>
<Text className="font-inter-medium">10. {words.at(9)}</Text>
<Text className="font-inter-medium">11. {words.at(10)}</Text>
<Text className="font-inter-medium">12. {words.at(11)}</Text>
</View>
</View>
<Pressable
className="mt-8 flex w-2/3 flex-row items-center justify-center self-center rounded-xl border-2 bg-white px-2 py-2 pt-3"
onPress={copyToClipboard}>
<Text className="mr-4 font-inter-medium text-black">Copy to clipboard!</Text>
<MonoIcon iconName="ClipboardCopy" />
</Pressable>
<View className="left-0 right-0 mt-8 flex flex-row">
<MonoIcon color="#8D93A0" iconName="Info" />
<Text className="text-slate-400"> We use a standard BIP84 derivation path</Text>
</View>
</View>
);
};

export default RecoveryPhraseDisplayComponent;
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { useEffect, useState } from 'react';
import { Text, View } from 'utils/wrappers/styled-react-native';

type Props = {
setPhraseValid: (valid: boolean) => void;
setPhrase: (phrase: string) => void;
};

const RecoveryPhraseComponent = ({ setPhraseValid }: Props) => {
const RecoveryPhraseInputComponent = ({ setPhrase }: Props) => {
const [word, setWord] = useState('');
const [selected, setSelected] = useState<Map<number, string>>(new Map());
const [selected, setSelected] = useState<string[]>([]);

useEffect(() => {
const pieces = word.split(' ');
Expand All @@ -21,41 +21,34 @@ const RecoveryPhraseComponent = ({ setPhraseValid }: Props) => {
const allMatching = wordlist.filter(w => w.startsWith(piece));

if (allMatching.length === 1 && allMatching[0] === piece) {
setSelected(new Map(selected).set(selected.size, piece));
setSelected([...selected, piece]);
setWord('');
}

return;
}

const newSelection = new Map(selected);
const newSelection = [...selected, ...pieces.filter(p => wordlist.includes(p))];

for (const [index, piece] of pieces.entries()) {
const found = wordlist.find(w => w === piece);
found && newSelection.set(selected.size + index, found);
}

setSelected(new Map(newSelection));
setSelected(newSelection);
setWord('');
}, [word, selected]);

useEffect(() => {
if (selected.size === 12) {
setPhraseValid(true);
if (selected.length === 12) {
setPhrase(selected.join(' '));
return;
}

setPhraseValid(false);
}, [selected, setPhraseValid]);
}, [selected, setPhrase]);

const removeLast = () => {
selected.delete(selected.size - 1);
setSelected(new Map(selected));
const newSelected = [...selected];
newSelected.pop();
setSelected(newSelected);
};

const removeAll = () => {
selected.clear();
setSelected(new Map(selected));
setSelected([]);
};

return (
Expand All @@ -64,7 +57,7 @@ const RecoveryPhraseComponent = ({ setPhraseValid }: Props) => {
<View className="flex-1">
<Text className="font-inter-medium">Recovery phrase</Text>
<TextInputComponent
disabled={selected.size >= 12}
disabled={selected.length >= 12}
style="border-b-0"
value={word}
onChangeText={setWord}
Expand All @@ -75,21 +68,21 @@ const RecoveryPhraseComponent = ({ setPhraseValid }: Props) => {
</View>
</View>
<View className="mt-8 flex w-full flex-row justify-around">
<View className="flex w-[20vw] flex-col">
<Text className="font-inter-medium">1. {selected.get(0)}</Text>
<Text className="font-inter-medium">2. {selected.get(1)}</Text>
<Text className="font-inter-medium">3. {selected.get(2)}</Text>
<Text className="font-inter-medium">4. {selected.get(3)}</Text>
<Text className="font-inter-medium">5. {selected.get(4)}</Text>
<Text className="font-inter-medium">6. {selected.get(5)}</Text>
<View className="flex w-[25vw] flex-col">
<Text className="font-inter-medium">1. {selected.at(0)}</Text>
<Text className="font-inter-medium">2. {selected.at(1)}</Text>
<Text className="font-inter-medium">3. {selected.at(2)}</Text>
<Text className="font-inter-medium">4. {selected.at(3)}</Text>
<Text className="font-inter-medium">5. {selected.at(4)}</Text>
<Text className="font-inter-medium">6. {selected.at(5)}</Text>
</View>
<View className="flex w-[20vw] flex-col">
<Text className="font-inter-medium">7. {selected.get(6)}</Text>
<Text className="font-inter-medium">8. {selected.get(7)}</Text>
<Text className="font-inter-medium">9. {selected.get(8)}</Text>
<Text className="font-inter-medium">10. {selected.get(9)}</Text>
<Text className="font-inter-medium">11. {selected.get(10)}</Text>
<Text className="font-inter-medium">12. {selected.get(11)}</Text>
<View className="flex w-[25vw] flex-col">
<Text className="font-inter-medium">7. {selected.at(6)}</Text>
<Text className="font-inter-medium">8. {selected.at(7)}</Text>
<Text className="font-inter-medium">9. {selected.at(8)}</Text>
<Text className="font-inter-medium">10. {selected.at(9)}</Text>
<Text className="font-inter-medium">11. {selected.at(10)}</Text>
<Text className="font-inter-medium">12. {selected.at(11)}</Text>
</View>
</View>
<View className="mt-8 flex w-full flex-row justify-center">
Expand All @@ -100,8 +93,12 @@ const RecoveryPhraseComponent = ({ setPhraseValid }: Props) => {
<MonoIcon iconName="XCircle" />
</Button>
</View>
<View className="left-0 right-0 mt-8 flex flex-row">
<MonoIcon color="#8D93A0" iconName="Info" />
<Text className="text-slate-400"> We use a standard BIP84 derivation path</Text>
</View>
</View>
);
};

export default RecoveryPhraseComponent;
export default RecoveryPhraseInputComponent;
5 changes: 3 additions & 2 deletions apps/mobile/src/hooks/useDeriveBitcoinWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { signWithDeviceKeyNoAuth } from 'utils/auth';
import { publicKeyToBitcoinAddressP2WPKH } from 'utils/crypto/bitcoin-address';
import { useFailableAction } from './useFailable';

export const useCreateBitcoinWallet = () => {
export const useCreateBitcoinWallet = (naviagteBack: () => void) => {
const { perform } = useFailableAction();
const { user } = useAuthState();

Expand All @@ -34,7 +34,8 @@ export const useCreateBitcoinWallet = () => {
.andThen(deriveAndSavePurpose)
.andThen(deriveAndSaveCoinType)
.andThen(deriveAndSaveAccount)
.andThen(deriveAddresses)
.andThen(deriveAddresses),
naviagteBack
);
};

Expand Down
17 changes: 4 additions & 13 deletions apps/mobile/src/screens/home.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import { RefreshControl } from 'react-native';
import { RootStackParamList } from 'screens/main-navigation';
import { useBitcoinState } from 'state/bitcoin.state';
import { DerivedUntilLevel, useDeriveState } from 'state/derive.state';
import { ScrollView, Text, View } from 'utils/wrappers/styled-react-native';
import { ScrollView } from 'utils/wrappers/styled-react-native';

type Props = StackScreenProps<RootStackParamList, 'Home'>;

const Home = ({ navigation }: Props) => {
const createBitcoinWallet = useCreateBitcoinWallet();
const createBitcoinWallet = useCreateBitcoinWallet(() => navigation.navigate('SetupWallet'));
const { secret, derivedUntilLevel, name } = useDeriveState();
const { accounts, getAccountBalance, getTotalBalance, hasAddress } = useBitcoinState();
const [loading, setLoading] = useState(false);
Expand All @@ -42,23 +42,14 @@ const Home = ({ navigation }: Props) => {
);

return (
<LayoutComponent
hideBack
noPadding
style="pl-8"
settingsNavigate={() => navigation.navigate('Menu')}>
<ScrollView className="h-full" refreshControl={refreshControl}>
<LayoutComponent hideBack noPadding settingsNavigate={() => navigation.navigate('Menu')}>
<ScrollView className="h-full pl-8" refreshControl={refreshControl}>
<Title>Wallets</Title>

<Title style="mb-4">{getTotalBalance()} BTC</Title>
{loading || !hasAddress() ? (
<>
<LoadingWalletItem name={name} />
<View className="mr-8 flex items-center justify-center self-center">
<Text className="mt-24 flex items-center justify-center text-center font-manrope-bold text-blue-800">
Your Wallet is currently being created for you. This can take a few moments...
</Text>
</View>
</>
) : (
[...accounts].map(([key, _]) => (
Expand Down
Loading

0 comments on commit 329cda4

Please sign in to comment.