From 275ee431af6cbe40c320a22d672e409f93e909d7 Mon Sep 17 00:00:00 2001 From: Fahad-Mahmood Date: Wed, 13 Mar 2024 05:16:29 +0500 Subject: [PATCH 1/2] feat: connection provider and splash component created --- App.tsx | 9 ++++++--- src/components/Splash.tsx | 6 ++++++ src/providers/ConnectionProvider.tsx | 29 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 src/components/Splash.tsx create mode 100644 src/providers/ConnectionProvider.tsx diff --git a/App.tsx b/App.tsx index d3912345..1ceb7304 100644 --- a/App.tsx +++ b/App.tsx @@ -4,13 +4,16 @@ import {config} from './src/theme/config'; import {NavigationContainer} from '@react-navigation/native'; import OnBoardingNavigation from './src/navigation/OnBoarding'; +import {ConnectionProvider} from './src/providers/ConnectionProvider'; function App(): React.JSX.Element { return ( - - - + + + + + ); } diff --git a/src/components/Splash.tsx b/src/components/Splash.tsx new file mode 100644 index 00000000..703f4331 --- /dev/null +++ b/src/components/Splash.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import {Box} from '@gluestack-ui/themed'; + +export const Splash = () => { + return ; +}; diff --git a/src/providers/ConnectionProvider.tsx b/src/providers/ConnectionProvider.tsx new file mode 100644 index 00000000..cb0cfe8f --- /dev/null +++ b/src/providers/ConnectionProvider.tsx @@ -0,0 +1,29 @@ +import React, {useContext, createContext} from 'react'; + +interface Props { + children: React.ReactNode; +} +interface ContextProps { + loading: boolean; +} +const ConnectionContext = createContext({ + loading: false, +}); + +export const ConnectionProvider = ({children}: Props) => { + return ( + + {children} + + ); +}; + +export const useConnectionContext = () => { + const context = useContext(ConnectionContext); + if (context === undefined) { + throw new Error( + 'useConnectionContext must be used within ConnectionProvider', + ); + } + return context; +}; From 40ae815a213038ff3ab185a97385a239fd85080f Mon Sep 17 00:00:00 2001 From: Fahad-Mahmood Date: Thu, 14 Mar 2024 04:27:30 +0500 Subject: [PATCH 2/2] feat: validate pin function added to decrypt seed feat: VerifyPin screen added --- src/navigation/OnBoarding.tsx | 95 +++++++----- src/navigation/screenNames.ts | 2 + src/providers/ConnectionProvider.tsx | 43 +++++- src/screens/Home/Dashboard.tsx | 2 +- src/screens/Home/VerifyPin.tsx | 218 +++++++++++++++++++++++++++ src/utils/encryption.ts | 2 +- 6 files changed, 323 insertions(+), 39 deletions(-) create mode 100644 src/screens/Home/VerifyPin.tsx diff --git a/src/navigation/OnBoarding.tsx b/src/navigation/OnBoarding.tsx index ef926aac..2031424a 100644 --- a/src/navigation/OnBoarding.tsx +++ b/src/navigation/OnBoarding.tsx @@ -1,12 +1,15 @@ import * as React from 'react'; import {createNativeStackNavigator} from '@react-navigation/native-stack'; -import OnBoardingHome from '../screens/onboarding/OnBoardingHome'; -import CreateWallet from '../screens/onboarding/CreateWallet'; +import OnBoardingHome from '../screens/Onboarding/OnBoardingHome'; +import CreateWallet from '../screens/Onboarding/CreateWallet'; import {SCREEN_NAMES} from './screenNames'; -import ConfirmSeed from '../screens/onboarding/ConfirmSeed'; -import PinSetup from '../screens/onboarding/PinSetup'; -import ConfirmPin from '../screens/onboarding/ConfirmPin'; +import ConfirmSeed from '../screens/Onboarding/ConfirmSeed'; +import PinSetup from '../screens/Onboarding/PinSetup'; +import ConfirmPin from '../screens/Onboarding/ConfirmPin'; import Dashboard from '../screens/Home/Dashboard'; +import {useConnectionContext} from '../providers/ConnectionProvider'; +import {Splash} from '../components/Splash'; +import VerifyPin from '../screens/Home/VerifyPin'; export type RootStackParamList = { OnboardingHome: undefined; @@ -15,43 +18,65 @@ export type RootStackParamList = { PinSetup: {words: string[]}; ConfirmPin: {words: string[]; walletPin: number[]}; Dashboard: undefined; + VerifyPin: undefined; }; const Stack = createNativeStackNavigator(); function OnBoardingNavigation() { + const {loading, isWalletConnected} = useConnectionContext(); + if (loading) { + return ; + } return ( - - - - - - + {isWalletConnected ? ( + + + + + ) : ( + + + + + + + + + )} ); } diff --git a/src/navigation/screenNames.ts b/src/navigation/screenNames.ts index 89404632..2a433898 100644 --- a/src/navigation/screenNames.ts +++ b/src/navigation/screenNames.ts @@ -5,6 +5,7 @@ interface IScreenNames { PinSetup: 'PinSetup'; ConfirmPin: 'ConfirmPin'; Dashboard: 'Dashboard'; + VerifyPin: 'VerifyPin'; } export const SCREEN_NAMES: IScreenNames = { OnboardingHome: 'OnboardingHome', @@ -13,4 +14,5 @@ export const SCREEN_NAMES: IScreenNames = { PinSetup: 'PinSetup', ConfirmPin: 'ConfirmPin', Dashboard: 'Dashboard', + VerifyPin: 'VerifyPin', }; diff --git a/src/providers/ConnectionProvider.tsx b/src/providers/ConnectionProvider.tsx index cb0cfe8f..5a911d7f 100644 --- a/src/providers/ConnectionProvider.tsx +++ b/src/providers/ConnectionProvider.tsx @@ -1,18 +1,57 @@ -import React, {useContext, createContext} from 'react'; +import React, {useContext, createContext, useEffect, useState} from 'react'; +import storage from '../utils/storage'; +import {STORAGE_KEYS} from '../utils/storage/storageKeys'; +import {decryptAesGcm} from '../utils/encryption'; interface Props { children: React.ReactNode; } interface ContextProps { loading: boolean; + isWalletConnected: boolean; + validatePin: (pin: string) => boolean; + seed: string | undefined; } const ConnectionContext = createContext({ loading: false, + isWalletConnected: false, + validatePin: () => false, + seed: undefined, }); export const ConnectionProvider = ({children}: Props) => { + const [loading, setLoading] = useState(false); + const [encryptedSeed, setEncryptedSeed] = useState(); + const [seed, setSeed] = useState(); + + useEffect(() => { + const loadWallet = async () => { + setLoading(true); + try { + const data = await storage.load({key: STORAGE_KEYS.seedCipher}); + setEncryptedSeed(data); + } catch (error) { + console.log(error); + } + + setLoading(false); + }; + loadWallet(); + }, []); + + const validatePin = (pin: string) => { + if (encryptedSeed) { + const decryptedSeed = decryptAesGcm(encryptedSeed, pin); + if (decryptedSeed) { + setSeed(decryptedSeed); + return true; + } + } + return false; + }; return ( - + {children} ); diff --git a/src/screens/Home/Dashboard.tsx b/src/screens/Home/Dashboard.tsx index f8bbf8ee..0d0c3ca3 100644 --- a/src/screens/Home/Dashboard.tsx +++ b/src/screens/Home/Dashboard.tsx @@ -5,7 +5,7 @@ import {NavigationProp} from '@react-navigation/native'; const LogoImage = require('../../assets/images/logo.png'); const HEADING_TEXT_1 = 'Firebolt Wallet'; -const HEADING_TEXT_2 = 'Dashboard'; +const HEADING_TEXT_2 = 'Connected!'; interface Props { navigation: NavigationProp; diff --git a/src/screens/Home/VerifyPin.tsx b/src/screens/Home/VerifyPin.tsx new file mode 100644 index 00000000..8c60a77f --- /dev/null +++ b/src/screens/Home/VerifyPin.tsx @@ -0,0 +1,218 @@ +import * as React from 'react'; +import {useState, useEffect} from 'react'; +import { + Box, + Text, + Heading, + HStack, + Center, + Button, + ButtonText, + ButtonIcon, + RepeatIcon, + CloseIcon, + useToast, + ButtonSpinner, +} from '@gluestack-ui/themed'; +import {SCREEN_NAMES} from '../../navigation/screenNames'; +import {RootStackParamList} from '../../navigation/OnBoarding'; +import type {NativeStackScreenProps} from '@react-navigation/native-stack'; + +import ShowToast from '../../components/ShowToast'; +import {useConnectionContext} from '../../providers/ConnectionProvider'; + +const HEADING_TEXT_1 = 'Enter'; +const HEADING_TEXT_2 = 'Your PIN code'; +const VERIFY_SEED_TEXT = 'Please enter your 5 digit wallet PIN code!'; + +const SUBMIT_BTN_TEXT = 'Access Wallet'; + +type Props = NativeStackScreenProps; + +function VerifyPin({navigation}: Props) { + const [verifyWalletPin, setVerifyWalletPin] = useState<(number | null)[]>( + Array(5).fill(null), + ); + const [buttonLoading, setButtonLoading] = useState(false); + + const {validatePin} = useConnectionContext(); + + const toast = useToast(); + const digits = React.useMemo( + () => [1, 2, 3, 4, 5, 6, 7, 8, 9, '', 0, 'X'], + [], + ); + + const onResetPress = () => { + setVerifyWalletPin(Array(5).fill(null)); + }; + + const onDigitPress = (digit: number | string) => { + if (typeof digit === 'number') { + const emptyIndex = verifyWalletPin.findIndex(item => item === null); + if (emptyIndex !== -1) { + const newWalletPin = [...verifyWalletPin]; + newWalletPin[emptyIndex] = digit; + setVerifyWalletPin(newWalletPin); + } + } else if (digit === 'X') { + const emptyIndex = verifyWalletPin.findIndex(item => item === null); + if (emptyIndex !== 0) { + const digitRemovalIndex = emptyIndex === -1 ? 4 : emptyIndex - 1; + const newWalletPin = [...verifyWalletPin]; + newWalletPin[digitRemovalIndex] = null; + setVerifyWalletPin(newWalletPin); + } + } + }; + + const verifySeedPin = () => { + const pin = verifyWalletPin.join('').toString(); + const result = validatePin(pin); + console.warn(result, 'result'); + if (!result) { + toast.show({ + placement: 'top', + render: ({id}) => ( + + ), + }); + onResetPress(); + } else { + navigation.navigate(SCREEN_NAMES.Dashboard); + } + setButtonLoading(false); + }; + + useEffect(() => { + if (buttonLoading) { + setTimeout(verifySeedPin, 1000); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [buttonLoading]); + + const onSubmit = async () => { + setButtonLoading(true); + }; + return ( + + + {HEADING_TEXT_1} + + + {HEADING_TEXT_2} + + + {VERIFY_SEED_TEXT} + + + + + + {verifyWalletPin.map((item, index) => ( + + + {item} + + + ))} + + + {digits.map((item, index) => ( + + + + ))} + +
+ +
+
+ ); +} + +export default VerifyPin; diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index b792d9fa..b7da7c60 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -161,6 +161,6 @@ export function decryptAesGcm( } catch (error) { console.error('Decryption failed!'); console.error(error); - return void 0; + return undefined; } }