Skip to content

Commit

Permalink
Import account with a Nostr Private key (#234)
Browse files Browse the repository at this point in the history
* import account screen in login

* add nav in create account + yarn format

* yarn format

* yarn lint

* change request
  • Loading branch information
MSghais authored Jul 22, 2024
1 parent f6b0651 commit 1875d32
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 2 deletions.
2 changes: 2 additions & 0 deletions JoyboyCommunity/src/app/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {StyleSheet, View} from 'react-native';
import {Icon} from '../components';
import {useStyles, useTheme} from '../hooks';
import {CreateAccount} from '../screens/Auth/CreateAccount';
import {ImportKeys} from '../screens/Auth/ImportKeys';
import {Login} from '../screens/Auth/Login';
import {SaveKeys} from '../screens/Auth/SaveKeys';
import {CreatePost} from '../screens/CreatePost';
Expand Down Expand Up @@ -117,6 +118,7 @@ const AuthNavigator: React.FC = () => {
{publicKey && <AuthStack.Screen name="Login" component={Login} />}
<AuthStack.Screen name="CreateAccount" component={CreateAccount} />
<AuthStack.Screen name="SaveKeys" component={SaveKeys} />
<AuthStack.Screen name="ImportKeys" component={ImportKeys} />
</AuthStack.Navigator>
);
};
Expand Down
8 changes: 7 additions & 1 deletion JoyboyCommunity/src/screens/Auth/CreateAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {useState} from 'react';
import {Platform} from 'react-native';

import {LockIcon} from '../../assets/icons';
import {Button, Input} from '../../components';
import {Button, Input, TextButton} from '../../components';
import {useNostrContext} from '../../context/NostrContext';
import {useTheme} from '../../hooks';
import {useDialog, useToast} from '../../hooks/modals';
Expand Down Expand Up @@ -70,6 +70,10 @@ export const CreateAccount: React.FC<AuthCreateAccountScreenProps> = ({navigatio
navigation.navigate('SaveKeys', {privateKey, publicKey});
};

const handleImportKey = () => {
navigation.navigate('ImportKeys');
};

return (
<Auth title="Create Account">
<Input placeholder="@ Username" value={username} onChangeText={setUsername} />
Expand All @@ -90,6 +94,8 @@ export const CreateAccount: React.FC<AuthCreateAccountScreenProps> = ({navigatio
>
Create Account
</Button>

<TextButton onPress={handleImportKey}>Import account</TextButton>
</Auth>
);
};
95 changes: 95 additions & 0 deletions JoyboyCommunity/src/screens/Auth/ImportKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {canUseBiometricAuthentication} from 'expo-secure-store';
import {useState} from 'react';
import {Platform} from 'react-native';

import {LockIcon} from '../../assets/icons';
import {Button, Input} from '../../components';
import {useTheme} from '../../hooks';
import {useDialog, useToast} from '../../hooks/modals';
import {Auth} from '../../modules/Auth';
import {AuthImportKeysScreenProps} from '../../types';
import {getPublicKeyFromSecret, isValidNostrPrivateKey} from '../../utils/keypair';
import {storePassword, storePrivateKey, storePublicKey} from '../../utils/storage';

export const ImportKeys: React.FC<AuthImportKeysScreenProps> = ({navigation}) => {
const {theme} = useTheme();

const [password, setPassword] = useState('');
const [privateKey, setPrivateKey] = useState('');
const {showToast} = useToast();
const {showDialog, hideDialog} = useDialog();

const handleImportAccount = async () => {
if (!password) {
showToast({type: 'error', title: 'Password is required'});
return;
}

if (!privateKey) {
showToast({type: 'error', title: 'Private key to import is required'});
return;
}

if (!isValidNostrPrivateKey(privateKey)) {
showToast({type: 'error', title: 'Private key not valid'});
return;
}
await storePrivateKey(privateKey, password);
const publicKey = getPublicKeyFromSecret(privateKey);
await storePublicKey(publicKey);

const biometySupported = Platform.OS !== 'web' && canUseBiometricAuthentication();
if (biometySupported) {
showDialog({
title: 'Easy login',
description: 'Would you like to use biometrics to login?',
buttons: [
{
type: 'primary',
label: 'Yes',
onPress: async () => {
await storePassword(password);
hideDialog();
},
},
{
type: 'default',
label: 'No',
onPress: hideDialog,
},
],
});
}

navigation.navigate('SaveKeys', {privateKey, publicKey});
};

return (
<Auth title="ImportKeys">
<Input
left={<LockIcon color={theme.colors.primary} />}
value={privateKey}
onChangeText={setPrivateKey}
secureTextEntry
placeholder="Private key"
/>

<Input
left={<LockIcon color={theme.colors.primary} />}
value={password}
onChangeText={setPassword}
secureTextEntry
placeholder="Password"
/>

<Button
block
variant="secondary"
disabled={!password || !privateKey}
onPress={handleImportAccount}
>
Import Account
</Button>
</Auth>
);
};
22 changes: 21 additions & 1 deletion JoyboyCommunity/src/screens/Auth/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ export const Login: React.FC<AuthLoginScreenProps> = ({navigation}) => {
});
};

const handleImportAccount = () => {
showDialog({
title: 'WARNING',
description:
'Creating a new account will delete your current account. Are you sure you want to continue?',
buttons: [
{
type: 'primary',
label: 'Continue',
onPress: () => {
navigation.navigate('ImportKeys');
hideDialog();
},
},
{type: 'default', label: 'Cancel', onPress: hideDialog},
],
});
};

return (
<Auth title="Login">
<Input
Expand All @@ -92,8 +111,9 @@ export const Login: React.FC<AuthLoginScreenProps> = ({navigation}) => {
<Button block variant="secondary" disabled={!password?.length} onPress={handleLogin}>
Login
</Button>

<TextButton onPress={handleCreateAccount}>Create Account</TextButton>

<TextButton onPress={handleImportAccount}>Import Account</TextButton>
</Auth>
);
};
6 changes: 6 additions & 0 deletions JoyboyCommunity/src/types/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type AuthStackParams = {
privateKey: string;
publicKey: string;
};
ImportKeys: undefined;
};

export type MainStackParams = {
Expand Down Expand Up @@ -49,6 +50,11 @@ export type AuthSaveKeysScreenProps = CompositeScreenProps<
NativeStackScreenProps<RootStackParams>
>;

export type AuthImportKeysScreenProps = CompositeScreenProps<
NativeStackScreenProps<AuthStackParams, 'ImportKeys'>,
NativeStackScreenProps<RootStackParams>
>;

// Home Stack
export type HomeNavigationProp = NativeStackNavigationProp<HomeBottomStackParams>;

Expand Down
15 changes: 15 additions & 0 deletions JoyboyCommunity/src/utils/keypair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,18 @@ export const getPublicKeyFromSecret = (privateKey: string) => {
throw new Error('Failed to get public key from secret key');
}
};

export const isValidNostrPrivateKey = (key: string): boolean => {
// Check if the string is exactly 64 characters long
if (key.length !== 64) {
return false;
}

// Check if the string contains only hexadecimal characters
const hexRegex = /^[0-9a-fA-F]+$/;
if (!hexRegex.test(key)) {
return false;
}

return true;
};

0 comments on commit 1875d32

Please sign in to comment.