From 1875d32fba97fc9c0ad4467a409f1988f80dcb37 Mon Sep 17 00:00:00 2001 From: MSG <59928086+MSghais@users.noreply.github.com> Date: Mon, 22 Jul 2024 18:35:32 +0200 Subject: [PATCH 1/3] Import account with a Nostr Private key (#234) * import account screen in login * add nav in create account + yarn format * yarn format * yarn lint * change request --- JoyboyCommunity/src/app/Router.tsx | 2 + .../src/screens/Auth/CreateAccount.tsx | 8 +- .../src/screens/Auth/ImportKeys.tsx | 95 +++++++++++++++++++ JoyboyCommunity/src/screens/Auth/Login.tsx | 22 ++++- JoyboyCommunity/src/types/routes.ts | 6 ++ JoyboyCommunity/src/utils/keypair.ts | 15 +++ 6 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 JoyboyCommunity/src/screens/Auth/ImportKeys.tsx diff --git a/JoyboyCommunity/src/app/Router.tsx b/JoyboyCommunity/src/app/Router.tsx index 18f8ace7..3d5408c0 100644 --- a/JoyboyCommunity/src/app/Router.tsx +++ b/JoyboyCommunity/src/app/Router.tsx @@ -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'; @@ -117,6 +118,7 @@ const AuthNavigator: React.FC = () => { {publicKey && } + ); }; diff --git a/JoyboyCommunity/src/screens/Auth/CreateAccount.tsx b/JoyboyCommunity/src/screens/Auth/CreateAccount.tsx index 758fb667..96fc4904 100644 --- a/JoyboyCommunity/src/screens/Auth/CreateAccount.tsx +++ b/JoyboyCommunity/src/screens/Auth/CreateAccount.tsx @@ -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'; @@ -70,6 +70,10 @@ export const CreateAccount: React.FC = ({navigatio navigation.navigate('SaveKeys', {privateKey, publicKey}); }; + const handleImportKey = () => { + navigation.navigate('ImportKeys'); + }; + return ( @@ -90,6 +94,8 @@ export const CreateAccount: React.FC = ({navigatio > Create Account + + Import account ); }; diff --git a/JoyboyCommunity/src/screens/Auth/ImportKeys.tsx b/JoyboyCommunity/src/screens/Auth/ImportKeys.tsx new file mode 100644 index 00000000..7932f5f7 --- /dev/null +++ b/JoyboyCommunity/src/screens/Auth/ImportKeys.tsx @@ -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 = ({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 ( + + } + value={privateKey} + onChangeText={setPrivateKey} + secureTextEntry + placeholder="Private key" + /> + + } + value={password} + onChangeText={setPassword} + secureTextEntry + placeholder="Password" + /> + + + + ); +}; diff --git a/JoyboyCommunity/src/screens/Auth/Login.tsx b/JoyboyCommunity/src/screens/Auth/Login.tsx index 81b2041e..6bb5bd45 100644 --- a/JoyboyCommunity/src/screens/Auth/Login.tsx +++ b/JoyboyCommunity/src/screens/Auth/Login.tsx @@ -79,6 +79,25 @@ export const Login: React.FC = ({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 ( = ({navigation}) => { - Create Account + + Import Account ); }; diff --git a/JoyboyCommunity/src/types/routes.ts b/JoyboyCommunity/src/types/routes.ts index cabd19b8..68225b2c 100644 --- a/JoyboyCommunity/src/types/routes.ts +++ b/JoyboyCommunity/src/types/routes.ts @@ -14,6 +14,7 @@ export type AuthStackParams = { privateKey: string; publicKey: string; }; + ImportKeys: undefined; }; export type MainStackParams = { @@ -49,6 +50,11 @@ export type AuthSaveKeysScreenProps = CompositeScreenProps< NativeStackScreenProps >; +export type AuthImportKeysScreenProps = CompositeScreenProps< + NativeStackScreenProps, + NativeStackScreenProps +>; + // Home Stack export type HomeNavigationProp = NativeStackNavigationProp; diff --git a/JoyboyCommunity/src/utils/keypair.ts b/JoyboyCommunity/src/utils/keypair.ts index 34a39d83..3a6818a0 100644 --- a/JoyboyCommunity/src/utils/keypair.ts +++ b/JoyboyCommunity/src/utils/keypair.ts @@ -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; +}; From e31f2679ab21479e1372331c18848b4bbb8677cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?U=C4=9Fur=20Eren?= Date: Tue, 23 Jul 2024 06:27:07 +0300 Subject: [PATCH 2/3] Fix: follow & like & comment issues (#246) * fix: follow & like & comment * fix: follow created_at --- JoyboyCommunity/src/hooks/nostr/useContacts.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useEditContacts.ts | 5 +++++ JoyboyCommunity/src/hooks/nostr/useNote.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useProfile.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useReactions.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useReplyNotes.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useReposts.ts | 2 +- JoyboyCommunity/src/hooks/nostr/useRootNotes.ts | 2 +- JoyboyCommunity/src/screens/Profile/Info/index.tsx | 12 +++++++++--- 9 files changed, 21 insertions(+), 10 deletions(-) diff --git a/JoyboyCommunity/src/hooks/nostr/useContacts.ts b/JoyboyCommunity/src/hooks/nostr/useContacts.ts index edd8aa29..86d99b22 100644 --- a/JoyboyCommunity/src/hooks/nostr/useContacts.ts +++ b/JoyboyCommunity/src/hooks/nostr/useContacts.ts @@ -12,7 +12,7 @@ export const useContacts = (options?: UseContactsOptions) => { const {ndk} = useNostrContext(); return useQuery({ - queryKey: ['contacts', ndk, options?.authors, options?.search], + queryKey: ['contacts', options?.authors, options?.search, ndk], queryFn: async () => { const contacts = await ndk.fetchEvent({ kinds: [NDKKind.Contacts], diff --git a/JoyboyCommunity/src/hooks/nostr/useEditContacts.ts b/JoyboyCommunity/src/hooks/nostr/useEditContacts.ts index 03418572..8b38d15b 100644 --- a/JoyboyCommunity/src/hooks/nostr/useEditContacts.ts +++ b/JoyboyCommunity/src/hooks/nostr/useEditContacts.ts @@ -23,12 +23,17 @@ export const useEditContacts = () => { contacts.tags = []; } + // Resetting the id and created_at to avoid conflicts + contacts.id = undefined as any; + contacts.created_at = undefined; + if (data.type === 'add') { contacts.tags.push(['p', data.pubkey, '', '']); } else { contacts.tags = contacts.tags.filter((tag) => tag[1] !== data.pubkey); } + await contacts.sign(); return contacts.publish(); }, }); diff --git a/JoyboyCommunity/src/hooks/nostr/useNote.ts b/JoyboyCommunity/src/hooks/nostr/useNote.ts index b5272761..bdacc1f7 100644 --- a/JoyboyCommunity/src/hooks/nostr/useNote.ts +++ b/JoyboyCommunity/src/hooks/nostr/useNote.ts @@ -11,7 +11,7 @@ export const useNote = (options: UseNoteOptions) => { const {ndk} = useNostrContext(); return useQuery({ - queryKey: ['note', ndk, options.noteId], + queryKey: ['note', options.noteId, ndk], queryFn: async () => { const note = await ndk.fetchEvent({ kinds: [NDKKind.Text], diff --git a/JoyboyCommunity/src/hooks/nostr/useProfile.ts b/JoyboyCommunity/src/hooks/nostr/useProfile.ts index 3df26734..3b2110d0 100644 --- a/JoyboyCommunity/src/hooks/nostr/useProfile.ts +++ b/JoyboyCommunity/src/hooks/nostr/useProfile.ts @@ -10,7 +10,7 @@ export const useProfile = (options: UseProfileOptions) => { const {ndk} = useNostrContext(); return useQuery({ - queryKey: ['profile', ndk, options.publicKey], + queryKey: ['profile', options.publicKey, ndk], queryFn: async () => { const user = ndk.getUser({pubkey: options.publicKey}); diff --git a/JoyboyCommunity/src/hooks/nostr/useReactions.ts b/JoyboyCommunity/src/hooks/nostr/useReactions.ts index c5d1c5ba..d58dee34 100644 --- a/JoyboyCommunity/src/hooks/nostr/useReactions.ts +++ b/JoyboyCommunity/src/hooks/nostr/useReactions.ts @@ -13,7 +13,7 @@ export const useReactions = (options?: UseReactionsOptions) => { const {ndk} = useNostrContext(); return useQuery({ - queryKey: ['reactions', ndk, options?.noteId, options?.authors, options?.search], + queryKey: ['reactions', options?.noteId, options?.authors, options?.search, ndk], queryFn: async () => { const notes = await ndk.fetchEvents({ kinds: [NDKKind.Reaction], diff --git a/JoyboyCommunity/src/hooks/nostr/useReplyNotes.ts b/JoyboyCommunity/src/hooks/nostr/useReplyNotes.ts index a3195fa3..6fed31f2 100644 --- a/JoyboyCommunity/src/hooks/nostr/useReplyNotes.ts +++ b/JoyboyCommunity/src/hooks/nostr/useReplyNotes.ts @@ -14,7 +14,7 @@ export const useReplyNotes = (options?: UseReplyNotesOptions) => { return useInfiniteQuery({ initialPageParam: 0, - queryKey: ['replyNotes', ndk, options?.noteId, options?.authors, options?.search], + queryKey: ['replyNotes', options?.noteId, options?.authors, options?.search, ndk], getNextPageParam: (lastPage: any, allPages, lastPageParam) => { if (!lastPage?.length) return undefined; diff --git a/JoyboyCommunity/src/hooks/nostr/useReposts.ts b/JoyboyCommunity/src/hooks/nostr/useReposts.ts index b11b2c0e..1320a52d 100644 --- a/JoyboyCommunity/src/hooks/nostr/useReposts.ts +++ b/JoyboyCommunity/src/hooks/nostr/useReposts.ts @@ -13,7 +13,7 @@ export const useReposts = (options?: UseRepostsOptions) => { return useInfiniteQuery({ initialPageParam: 0, - queryKey: ['reposts', ndk, options?.authors, options?.search], + queryKey: ['reposts', options?.authors, options?.search, ndk], getNextPageParam: (lastPage: any, allPages, lastPageParam) => { if (!lastPage?.length) return undefined; diff --git a/JoyboyCommunity/src/hooks/nostr/useRootNotes.ts b/JoyboyCommunity/src/hooks/nostr/useRootNotes.ts index 1e687b53..c04b2343 100644 --- a/JoyboyCommunity/src/hooks/nostr/useRootNotes.ts +++ b/JoyboyCommunity/src/hooks/nostr/useRootNotes.ts @@ -13,7 +13,7 @@ export const useRootNotes = (options?: UseRootNotesOptions) => { return useInfiniteQuery({ initialPageParam: 0, - queryKey: ['rootNotes', ndk, options?.authors, options?.search], + queryKey: ['rootNotes', options?.authors, options?.search, ndk], getNextPageParam: (lastPage: any, allPages, lastPageParam) => { if (!lastPage?.length) return undefined; diff --git a/JoyboyCommunity/src/screens/Profile/Info/index.tsx b/JoyboyCommunity/src/screens/Profile/Info/index.tsx index d750cb9e..d5a9ae12 100644 --- a/JoyboyCommunity/src/screens/Profile/Info/index.tsx +++ b/JoyboyCommunity/src/screens/Profile/Info/index.tsx @@ -6,6 +6,7 @@ import {Pressable, View} from 'react-native'; import {UserPlusIcon} from '../../../assets/icons'; import {Button, IconButton, Menu, Text} from '../../../components'; import {useContacts, useEditContacts, useProfile, useStyles, useTheme} from '../../../hooks'; +import {useToast} from '../../../hooks/modals'; import {useAuth} from '../../../store/auth'; import {ProfileScreenProps} from '../../../types'; import {ProfileHead} from '../Head'; @@ -26,6 +27,7 @@ export const ProfileInfo: React.FC = ({publicKey: userPublicKe const [menuOpen, setMenuOpen] = useState(false); const publicKey = useAuth((state) => state.publicKey); + const {showToast} = useToast(); const queryClient = useQueryClient(); const userContacts = useContacts({authors: [userPublicKey]}); const contacts = useContacts({authors: [publicKey]}); @@ -40,12 +42,16 @@ export const ProfileInfo: React.FC = ({publicKey: userPublicKe const onConnectionPress = () => { editContacts.mutateAsync( - {pubkey: publicKey, type: isConnected ? 'remove' : 'add'}, + {pubkey: userPublicKey, type: isConnected ? 'remove' : 'add'}, { onSuccess: () => { queryClient.invalidateQueries({queryKey: ['contacts']}); - userContacts.refetch(); - contacts.refetch(); + }, + onError: () => { + showToast({ + type: 'error', + title: isConnected ? 'Failed to unfollow user' : 'Failed to follow user', + }); }, }, ); From bcf4927afdd7f04bae24fabbdb7d566d4f7cd3c8 Mon Sep 17 00:00:00 2001 From: Lau Chaves Date: Mon, 22 Jul 2024 21:58:27 -0600 Subject: [PATCH 3/3] Fix: claim button and menu diviver theme colors (#230) Co-authored-by: lauchaves --- JoyboyCommunity/src/components/Menu/styles.ts | 2 +- JoyboyCommunity/src/styles/Colors.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/JoyboyCommunity/src/components/Menu/styles.ts b/JoyboyCommunity/src/components/Menu/styles.ts index 2fd7b51a..3f2ae86f 100644 --- a/JoyboyCommunity/src/components/Menu/styles.ts +++ b/JoyboyCommunity/src/components/Menu/styles.ts @@ -28,7 +28,7 @@ export default ThemedStyleSheet((theme) => ({ position: 'absolute', height: 'auto', gap: StyleSheet.hairlineWidth, - backgroundColor: theme.colors.background, + backgroundColor: theme.colors.divider, borderRadius: 16, overflow: 'hidden', shadowColor: theme.colors.shadow, diff --git a/JoyboyCommunity/src/styles/Colors.tsx b/JoyboyCommunity/src/styles/Colors.tsx index 8e75336f..86793313 100644 --- a/JoyboyCommunity/src/styles/Colors.tsx +++ b/JoyboyCommunity/src/styles/Colors.tsx @@ -81,7 +81,7 @@ export const DarkTheme = { textSecondary: '#FFFFFF', textLight: '#FFFFFF', textStrong: '#FFFFFF', - onPrimary: '#8F979E', + onPrimary: '#FFFFFF', onSecondary: '#FFFFFF', divider: '#1b1b18',