diff --git a/App.tsx b/App.tsx index fea5e55a..06ce57b7 100644 --- a/App.tsx +++ b/App.tsx @@ -165,13 +165,13 @@ function App(): JSX.Element { - - - {/* + + {/* }> */} - - - + + + {/* */} @@ -535,7 +535,7 @@ function ResetStack(): JSX.Element | null { if (mnemonic.value && !storedSettings.isSecurityEnabled) { setAccountMnemonic(mnemonic.value); } - + setSecuritySettings(storedSettings); setInitSettings(prev => { return { ...prev, diff --git a/app/assets/icons/nwcLogo.png b/app/assets/icons/nwcLogo.png new file mode 100644 index 00000000..dd1e3afe Binary files /dev/null and b/app/assets/icons/nwcLogo.png differ diff --git a/app/components/admin/homeComponents/accounts/accountCard.js b/app/components/admin/homeComponents/accounts/accountCard.js new file mode 100644 index 00000000..c40d2e94 --- /dev/null +++ b/app/components/admin/homeComponents/accounts/accountCard.js @@ -0,0 +1,232 @@ +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import { + SIZES, + COLORS, + BASIC_ACCOUNT_NAME_REGEX, + ICONS, +} from '../../../../constants'; +import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; +import { useGlobalThemeContext } from '../../../../../context-store/theme'; +import { useTranslation } from 'react-i18next'; +import GetThemeColors from '../../../../hooks/themeColors'; +import AccountProfileImage from './accountProfileImage'; +import SkeletonPlaceholder from '../../../../functions/CustomElements/skeletonView'; + +/** + * Account card component for account management + * Shows account name, type (derived/imported), and balance + * Active accounts show a blue dot indicator + */ +export default function AccountCard({ + account, + isActive, + onPress, + onEdit, + isLoading, + useSelection = false, + fromSettings = false, +}) { + const { theme, darkModeType } = useGlobalThemeContext(); + const { backgroundColor, backgroundOffset, textColor } = GetThemeColors(); + const { t } = useTranslation(); + + const accountIndex = + account?.name === 'Main Wallet' + ? 1 + : account?.name === 'NWC' + ? 2 + : account?.derivationIndex; + + if (isLoading) { + return ( + + + + + + + + + ); + } + + return ( + + {/* Left: Account Badge */} + + + + + {isActive && ( + + + + )} + + + {/* Middle: Account Name + Meta */} + + + + + {/* Right: Edit Button or Select Text */} + {useSelection && ( + + + + + + )} + {account.name !== 'Main Wallet' && + account.name !== 'NWC' && + !useSelection && ( + + + + + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { + flexDirection: 'row', + alignItems: 'center', + minHeight: 50, + borderRadius: 8, + paddingVertical: 10, + gap: 12, + }, + leftSection: { + alignItems: 'center', + justifyContent: 'center', + }, + badge: { + width: 40, + height: 40, + borderRadius: 22.5, + alignItems: 'center', + justifyContent: 'center', + }, + middleSection: { + flex: 1, + }, + activeDot: { + width: 18, + height: 18, + borderRadius: 10, + position: 'absolute', + bottom: -5, + right: -2, + alignItems: 'center', + justifyContent: 'center', + }, + accountName: { + flexShrink: 1, + fontSize: SIZES.medium, + includeFontPadding: false, + }, + rightSection: { + alignItems: 'flex-end', + justifyContent: 'center', + borderRadius: 8, + }, + editButton: { + height: 40, + width: 40, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, + // Skeleton styles + skeletonContainer: { + flexDirection: 'row', + alignItems: 'center', + width: 250, + height: 40, + }, + skeletonBadge: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + }, + skeletonText: { + width: 150, + height: 20, + borderRadius: 4, + }, +}); diff --git a/app/components/admin/homeComponents/accounts/accountProfileImage.js b/app/components/admin/homeComponents/accounts/accountProfileImage.js new file mode 100644 index 00000000..1c594884 --- /dev/null +++ b/app/components/admin/homeComponents/accounts/accountProfileImage.js @@ -0,0 +1,63 @@ +import { Image } from 'expo-image'; +import { StyleSheet, View } from 'react-native'; +import { useGlobalContextProvider } from '../../../../../context-store/context'; +import { useImageCache } from '../../../../../context-store/imageCache'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { COLORS, ICONS } from '../../../../constants'; +import { ThemeText } from '../../../../functions/CustomElements'; +import ContactProfileImage from '../contacts/internalComponents/profileImage'; +import { useGlobalThemeContext } from '../../../../../context-store/theme'; + +export default function AccountProfileImage({ account, imageSize }) { + const { cache } = useImageCache(); + const { masterInfoObject } = useGlobalContextProvider(); + const { textColor } = GetThemeColors(); + const { theme, darkModeType } = useGlobalThemeContext(); + + const uri = + account.name === 'Main Wallet' + ? cache[masterInfoObject.uuid]?.localUri + : account.profileImage; + const updated = + account.name === 'Main Wallet' + ? cache[masterInfoObject.uuid]?.updated + : account.timeUploaded; + + return ( + + {account.name === 'NWC' ? ( + + ) : account.profileEmoji ? ( + + ) : ( + + )} + + ); +} +const styles = StyleSheet.create({ + badge: { + width: '100%', + height: '100%', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 1, + }, +}); diff --git a/app/components/admin/homeComponents/contacts/contactsPage.js b/app/components/admin/homeComponents/contacts/contactsPage.js index 37e3994a..9c7b2f6f 100644 --- a/app/components/admin/homeComponents/contacts/contactsPage.js +++ b/app/components/admin/homeComponents/contacts/contactsPage.js @@ -50,6 +50,7 @@ import { useGlobalInsets } from '../../../../../context-store/insetsProvider'; import { TAB_ITEM_HEIGHT } from '../../../../../navigation/tabs'; import { formatDisplayName } from './utils/formatListDisplayName'; import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; +import ProfileImageSettingsNavigator from '../../../../functions/CustomElements/profileSettingsNavigator'; export default function ContactsPage({ navigation }) { const { contactsPrivateKey, publicKey } = useKeysContext(); @@ -271,10 +272,6 @@ export default function ContactsPage({ navigation }) { t, ]); - const goToMyProfile = useCallback(() => { - keyboardNavigate(() => navigate.navigate('SettingsHome', {})); - }, [navigate]); - const handleButtonPress = useCallback(() => { if (!isConnectedToTheInternet) { navigate.navigate('ErrorScreen', { @@ -358,21 +355,7 @@ export default function ContactsPage({ navigation }) { content={t('contacts.contactsPage.contactsHeader')} styles={memoizedStyles.headerText} /> - - - - - + )} {hasContacts && didEditProfile ? ( diff --git a/app/components/admin/homeComponents/pools/poolDetailScreen.js b/app/components/admin/homeComponents/pools/poolDetailScreen.js index 36a42a21..438b5da6 100644 --- a/app/components/admin/homeComponents/pools/poolDetailScreen.js +++ b/app/components/admin/homeComponents/pools/poolDetailScreen.js @@ -75,7 +75,11 @@ export default function PoolDetailScreen(props) { const refreshPoolDetails = useCallback(async () => { try { - if (pools?.[poolId]?.status === 'closed') return; + // if ( + // pools?.[poolId]?.status === 'closed' && + // Date.now() - pools?.[poolId]?.closedAt > 10 * 1000 + // ) + // return; // Refresh pool data from Firestore const freshPool = await getPoolFromDatabase(poolId); if (freshPool) { @@ -137,8 +141,8 @@ export default function PoolDetailScreen(props) { }, [navigate, poolId, pool]); const handleContributorClick = useCallback(() => { - navigate.navigate('ViewContributor', { poolId, contributions }); - }, [poolId, contributions]); + navigate.navigate('ViewContributor', { pool, contributions }); + }, [pool, contributions]); const contributers = useMemo(() => { return [pool, ...contributions].map((item, index) => ( @@ -237,7 +241,7 @@ export default function PoolDetailScreen(props) { label={t('wallet.pools.pool')} showLeftImage={isCreator} leftImageStyles={{ height: 25 }} - iconNew={isActive ? 'Trash' : 'RefreshCcw'} + iconNew={isActive ? 'Trash2' : 'RefreshCcw'} leftImageFunction={isActive ? handleClosePool : handleReCheck} /> @@ -369,13 +373,12 @@ const styles = StyleSheet.create({ alignItems: 'center', marginTop: 25, }, - closedBannerText: { - marginBottom: 5, - }, + closedBannerText: {}, transferredText: { fontSize: SIZES.small, opacity: 0.7, textAlign: 'center', + marginTop: 5, }, actionsRow: { flexDirection: 'row', diff --git a/app/components/admin/homeComponents/pools/presetAmountGrid.js b/app/components/admin/homeComponents/pools/presetAmountGrid.js index fec363f2..bda2646e 100644 --- a/app/components/admin/homeComponents/pools/presetAmountGrid.js +++ b/app/components/admin/homeComponents/pools/presetAmountGrid.js @@ -24,9 +24,9 @@ export default function PresetAmountGrid({ fiatStats, onCustomPress, }) { - const { theme } = useGlobalThemeContext(); + const { theme, darkModeType } = useGlobalThemeContext(); const { masterInfoObject } = useGlobalContextProvider(); - const { backgroundOffset } = GetThemeColors(); + const { backgroundOffset, backgroundColor } = GetThemeColors(); const fiatPrice = fiatStats?.value || 0; const isFiatMode = masterInfoObject.userBalanceDenomination === 'fiat'; @@ -102,7 +102,11 @@ export default function PresetAmountGrid({ style={[ styles.presetButton, { - backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + backgroundColor: theme + ? darkModeType + ? backgroundColor + : backgroundOffset + : COLORS.darkModeText, borderColor: isCustomSelected() ? theme ? COLORS.darkModeText @@ -124,7 +128,11 @@ export default function PresetAmountGrid({ style={[ styles.presetButton, { - backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + backgroundColor: theme + ? darkModeType + ? backgroundColor + : backgroundOffset + : COLORS.darkModeText, borderColor: isPresetSelected(item) ? theme ? COLORS.darkModeText diff --git a/app/components/admin/homeComponents/pools/viewContributors.js b/app/components/admin/homeComponents/pools/viewContributors.js index 92a7ba12..26c59403 100644 --- a/app/components/admin/homeComponents/pools/viewContributors.js +++ b/app/components/admin/homeComponents/pools/viewContributors.js @@ -6,12 +6,7 @@ import { } from '../../../../functions/CustomElements'; import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar'; import { CENTER, CONTENT_KEYBOARD_OFFSET, SIZES } from '../../../../constants'; -import { - HIDDEN_OPACITY, - INSET_WINDOW_WIDTH, - WINDOWWIDTH, -} from '../../../../constants/theme'; -import { usePools } from '../../../../../context-store/poolContext'; +import { HIDDEN_OPACITY, WINDOWWIDTH } from '../../../../constants/theme'; import { useGlobalContextProvider } from '../../../../../context-store/context'; import ContributorAvatar from './contributorAvatar'; import CustomButton from '../../../../functions/CustomElements/button'; @@ -20,26 +15,26 @@ import displayCorrectDenomination from '../../../../functions/displayCorrectDeno import { useTranslation } from 'react-i18next'; export default function ViewContibutors(props) { - const poolId = props.route?.params?.poolId; + const pool = props.route?.params?.pool; const contributions = props.route?.params?.contributions; const { masterInfoObject } = useGlobalContextProvider(); - const { pools } = usePools(); const { fiatStats } = useNodeContext(); const { t } = useTranslation(); - const pool = pools[poolId]; const contributers = [pool, ...contributions]; + console.log(pool); const handleShare = useCallback(async () => { try { await Share.share({ - message: `https://blitzwalletapp.com/pools/${poolId}`, + message: `https://blitzwalletapp.com/pools/${pool.poolId}`, }); } catch (err) { console.log('Error sharing pool:', err); } - }, [poolId]); + }, [pool]); + console.log(contributers); const Contributor = useCallback(({ item, index }) => { if (!item) return; @@ -54,6 +49,7 @@ export default function ViewContibutors(props) { /> { + if ( + (transferType === 'from' && selectedFrom === account.uuid) || + (transferType === 'to' && selectedTo === account.uuid) + ) { + navigate.goBack(); + return; + } - const accountElements = accounts + setIsLoading({ + accountBeingLoaded: account.uuid, + isLoading: true, + }); + + const accountMnemoinic = await getAccountMnemonic(account); + + await new Promise(res => setTimeout(res, 800)); + await initializeSparkWallet(accountMnemoinic, false, { + maxRetries: 4, + }); + let balance = 0; + if (transferType === 'from') { + const balanceResponse = await getSparkBalance(accountMnemoinic); + balance = Number(balanceResponse.balance); + } + + navigate.popTo( + 'CustodyAccountPaymentPage', + { + [transferType]: account.uuid, + [`${transferType}Balance`]: balance, + }, + { + merge: true, + }, + ); + }, + [navigate, selectedFrom, selectedTo, transferType, getAccountMnemonic], + ); + + const accountElements = custodyAccountsList .filter(item => { return ( - item.mnemoinc !== (transferType === 'from' ? selectedTo : selectedFrom) + item.uuid !== (transferType === 'from' ? selectedTo : selectedFrom) ); }) .map((account, index) => { return ( - - - - { - if ( - (transferType === 'from' && - selectedFrom === account.mnemoinc) || - (transferType === 'to' && selectedTo === account.mnemoinc) - ) { - navigate.goBack(); - return; - } - - setIsLoading({ - accountBeingLoaded: account.mnemoinc, - isLoading: true, - }); - - await new Promise(res => setTimeout(res, 800)); - await initializeSparkWallet(account.mnemoinc, false, { - maxRetries: 4, - }); - let balance = 0; - if (transferType === 'from') { - const balanceResponse = await getSparkBalance(account.mnemoinc); - balance = Number(balanceResponse.balance); - } - - navigate.popTo( - 'CustodyAccountPaymentPage', - { - [transferType]: account.mnemoinc, - [`${transferType}Balance`]: balance, - }, - { - merge: true, - }, - ); - }} - buttonStyles={{ - width: 'auto', - backgroundColor: - theme && darkModeType ? backgroundOffset : backgroundColor, - }} - textStyles={{ color: textColor }} - loadingColor={textColor} - textContent={t('constants.select')} - useLoading={ - isLoading.accountBeingLoaded === account.mnemoinc && - isLoading.isLoading - } - /> - + handleAccountSelection(account)} + isLoading={ + isLoading.accountBeingLoaded === account.uuid && isLoading.isLoading + } + useSelection={true} + /> ); }); return ( - + {accountElements} @@ -125,7 +114,7 @@ const styles = StyleSheet.create({ sectionHeader: { width: '100%', fontSize: SIZES.large, - textAlign: 'center', + fontWeight: 500, marginBottom: 10, }, container: { flex: 1, width: INSET_WINDOW_WIDTH, ...CENTER }, diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/accountPaymentPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/accountPaymentPage.js index 81d0880f..915c6ff4 100644 --- a/app/components/admin/homeComponents/settingsContent/accountComponents/accountPaymentPage.js +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/accountPaymentPage.js @@ -24,7 +24,6 @@ import useDebounce from '../../../../../hooks/useDebounce'; import { useGlobalThemeContext } from '../../../../../../context-store/theme'; import GetThemeColors from '../../../../../hooks/themeColors'; import ThemeImage from '../../../../../functions/CustomElements/themeImage'; -import useCustodyAccountList from '../../../../../hooks/useCustodyAccountsList'; import { sparkPaymenWrapper } from '../../../../../functions/spark/payments'; import { useKeysContext } from '../../../../../../context-store/keys'; import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; @@ -48,7 +47,8 @@ export default function AccountPaymentPage(props) { const { masterInfoObject } = useGlobalContextProvider(); const { fiatStats } = useNodeContext(); const { theme, darkModeType } = useGlobalThemeContext(); - const { currentWalletMnemoinc } = useActiveCustodyAccount(); + const { currentWalletMnemoinc, getAccountMnemonic, custodyAccountsList } = + useActiveCustodyAccount(); const sendingAmount = props?.route?.params?.amount || 0; const from = props?.route?.params?.from; const to = props?.route?.params?.to; @@ -65,9 +65,10 @@ export default function AccountPaymentPage(props) { const { backgroundOffset, textColor } = GetThemeColors(); const { t } = useTranslation(); - const accounts = useCustodyAccountList(); - const fromAccount = accounts.find(item => item.mnemoinc === from)?.name || ''; - const toAccount = accounts.find(item => item.mnemoinc === to)?.name || ''; + const fromAccount = + custodyAccountsList.find(item => item.uuid === from)?.name || ''; + const toAccount = + custodyAccountsList.find(item => item.uuid === to)?.name || ''; const convertedSendAmount = masterInfoObject.userBalanceDenomination != 'fiat' @@ -146,7 +147,19 @@ export default function AccountPaymentPage(props) { } setTransferInfo(prev => ({ ...prev, isDoingTransfer: true })); - const toSparkAddress = await getSparkAddress(to); + const sendingFromAccount = custodyAccountsList.find( + item => item.uuid === from, + ); + const sendingToAccount = custodyAccountsList.find( + item => item.uuid === to, + ); + + const [fromMnemonic, toMnemonic] = await Promise.all([ + getAccountMnemonic(sendingFromAccount), + getAccountMnemonic(sendingToAccount), + ]); + + const toSparkAddress = await getSparkAddress(toMnemonic); if (!toSparkAddress.didWork) { throw new Error( @@ -156,8 +169,8 @@ export default function AccountPaymentPage(props) { const [accountIdentifyPubKey, toAccountIdentityPubKey] = await Promise.all([ - getSparkIdentityPubKey(from), - getSparkIdentityPubKey(to), + getSparkIdentityPubKey(fromMnemonic), + getSparkIdentityPubKey(toMnemonic), ]); if (!accountIdentifyPubKey || !toAccountIdentityPubKey) { @@ -183,7 +196,7 @@ export default function AccountPaymentPage(props) { sparkInformation: { identityPubKey: accountIdentifyPubKey, }, - mnemonic: from, + mnemonic: fromMnemonic, sendWebViewRequest, }); @@ -224,6 +237,7 @@ export default function AccountPaymentPage(props) { from, currentWalletMnemoinc, memo, + custodyAccountsList, ]); if (transferInfo?.showConfirmScreen) { @@ -327,7 +341,7 @@ export default function AccountPaymentPage(props) { onPress={() => { navigate.navigate('CustomHalfModal', { wantedContent: 'SelectAltAccount', - sliderHight: 0.5, + sliderHight: 0.6, selectedFrom: from, selectedTo: to, transferType: 'from', @@ -376,7 +390,7 @@ export default function AccountPaymentPage(props) { onPress={() => { navigate.navigate('CustomHalfModal', { wantedContent: 'SelectAltAccount', - sliderHight: 0.5, + sliderHight: 0.6, selectedFrom: from, selectedTo: to, transferType: 'to', diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/createAccountPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/createAccountPage.js index 901ea85d..9d554a0c 100644 --- a/app/components/admin/homeComponents/settingsContent/accountComponents/createAccountPage.js +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/createAccountPage.js @@ -11,17 +11,16 @@ import { ScrollView, StyleSheet, TextInput, - TouchableOpacity, View, } from 'react-native'; import { CENTER, CONTENT_KEYBOARD_OFFSET, + MAX_DERIVED_ACCOUNTS, SIZES, } from '../../../../../constants'; import CustomButton from '../../../../../functions/CustomElements/button'; import { useNavigation } from '@react-navigation/native'; -import { createAccountMnemonic } from '../../../../../functions'; import { COLORS, FONT, @@ -35,12 +34,11 @@ import SuggestedWordContainer from '../../../../login/suggestedWords'; import isValidMnemonic from '../../../../../functions/isValidMnemonic'; import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; import customUUID from '../../../../../functions/customUUID'; -import useCustodyAccountList from '../../../../../hooks/useCustodyAccountsList'; import { handleRestoreFromText } from '../../../../../functions/seed'; import getClipboardText from '../../../../../functions/getClipboardText'; import { useGlobalInsets } from '../../../../../../context-store/insetsProvider'; import { useTranslation } from 'react-i18next'; -import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import { useGlobalContextProvider } from '../../../../../../context-store/context'; const NUMARRAY = Array.from({ length: 12 }, (_, i) => i + 1); const INITIAL_KEY_STATE = NUMARRAY.reduce((acc, num) => { acc[`key${num}`] = ''; @@ -48,8 +46,9 @@ const INITIAL_KEY_STATE = NUMARRAY.reduce((acc, num) => { }, {}); export default function CreateCustodyAccountPage(props) { - const selectedAccount = props?.route?.params?.account; - const { createAccount, currentWalletMnemoinc, removeAccount, updateAccount } = + const accountType = props?.route?.params?.accountType || 'derived'; + const { masterInfoObject } = useGlobalContextProvider(); + const { createDerivedAccount, createImportedAccount, custodyAccountsList } = useActiveCustodyAccount(); const { theme, darkModeType } = useGlobalThemeContext(); const { bottomPadding } = useGlobalInsets(); @@ -57,36 +56,23 @@ export default function CreateCustodyAccountPage(props) { const [isKeyboardActive, setIsKeyboardActive] = useState(false); const [isCreatingAccount, setIsCreatingAccount] = useState(false); const [accountInformation, setAccountInformation] = useState({ - name: selectedAccount?.name || '', - mnemoinc: selectedAccount?.mnemoinc || '', - dateCreated: selectedAccount?.dateCreated || Date.now(), - password: selectedAccount?.password || '', - isPasswordEnabled: selectedAccount?.isPasswordEnabled || false, - uuid: selectedAccount?.uuid || customUUID(), - isActive: selectedAccount?.isActive || false, + name: '', + mnemoinc: '', + dateCreated: Date.now(), + password: '', + isPasswordEnabled: false, + uuid: customUUID(), + isActive: false, }); const [currentFocused, setCurrentFocused] = useState(null); const [inputedKey, setInputedKey] = useState(INITIAL_KEY_STATE); const keyRefs = useRef({}); - const blockDeleteAccountRef = useRef(null); const { backgroundOffset, textColor, textInputColor } = GetThemeColors(); - const accounts = useCustodyAccountList(); const navigate = useNavigation(); - const foundAccount = accounts.find( - account => - account.name.toLowerCase() === accountInformation.name.toLowerCase(), - ); - const deleatedAccount = !!accounts.find( - account => - account.name.toLowerCase() === selectedAccount?.name.toLowerCase(), - ); - const nameIsAlreadyUsed = - Boolean(foundAccount) && foundAccount?.name !== selectedAccount?.name; - const enteredAllSeeds = Object.values(inputedKey).filter(item => item); const { t } = useTranslation(); @@ -123,98 +109,81 @@ export default function CreateCustodyAccountPage(props) { [keyRefs], ); - useEffect(() => { - async function initalizeAccount() { - const mnemoinc = await (selectedAccount - ? Promise.resolve(selectedAccount.mnemoinc) - : createAccountMnemonic(true)); - const mnemoincArray = mnemoinc.split(' '); - const keyState = NUMARRAY.reduce((acc, num) => { - acc[`key${num}`] = mnemoincArray[num - 1]; - return acc; - }, {}); - setInputedKey(keyState); - setAccountInformation(prev => ({ - ...prev, - mnemoinc, - })); - } - initalizeAccount(); - }, []); - - useEffect(() => { - if (!blockDeleteAccountRef.current) { - blockDeleteAccountRef.current = true; - return; - } - - if (deleatedAccount) return; - navigate.goBack(); - }, [deleatedAccount]); - - const regenerateSeed = async () => { - const mnemoinc = await createAccountMnemonic(true); - const mnemoincArray = mnemoinc.split(' '); - const keyState = NUMARRAY.reduce((acc, num) => { - acc[`key${num}`] = mnemoincArray[num - 1]; - return acc; - }, {}); - setInputedKey(keyState); - setAccountInformation(prev => ({ - ...prev, - mnemoinc, - })); - }; - const handleCreateAccount = async () => { try { if (!accountInformation.name) return; - if (nameIsAlreadyUsed) return; - if (enteredAllSeeds.length !== 12) return; - if ( - selectedAccount?.name?.toLowerCase() === - accountInformation.name.toLowerCase() - ) { - navigate.goBack(); - return; - } - const isValidSeed = isValidMnemonic(enteredAllSeeds); - if (!isValidSeed) { - navigate.navigate('ErrorScreen', { - errorMessage: t('errormessages.invalidSeedError'), - }); - return; - } - const seedString = enteredAllSeeds.join(' '); - const alreadyUsedSeed = selectedAccount - ? false - : accounts.find( - account => account.mnemoinc.toLowerCase() === seedString, - ); - if (alreadyUsedSeed) { - navigate.navigate('ErrorScreen', { - errorMessage: t( - 'settings.accountComponents.createAccountPage.alreadyUsingSeedError', - ), - }); - return; - } + setIsCreatingAccount(true); - if (selectedAccount) { - const response = await updateAccount(accountInformation); - if (!response.didWork) throw new Error(response.err); + // Creating new account + if (accountType === 'derived') { + const nextIndex = Number( + masterInfoObject.nextAccountDerivationIndex || 0, + ); + if (nextIndex >= MAX_DERIVED_ACCOUNTS) { + throw new Error( + `Maximum of ${MAX_DERIVED_ACCOUNTS} accounts reached. Please delete unused accounts.`, + ); + } + + // Create derived account (no seed needed) + const response = await createDerivedAccount(accountInformation.name); + if (!response.didWork) { + setIsCreatingAccount(false); + navigate.navigate('ErrorScreen', { + errorMessage: response.error, + }); + return; + } } else { - const response = await createAccount({ - ...accountInformation, - mnemoinc: seedString, - }); - if (!response.didWork) throw new Error(response.err); + // Create imported account (requires seed validation) + if (enteredAllSeeds.length !== 12) { + setIsCreatingAccount(false); + return; + } + + const isValidSeed = isValidMnemonic(enteredAllSeeds); + if (!isValidSeed) { + setIsCreatingAccount(false); + navigate.navigate('ErrorScreen', { + errorMessage: t('errormessages.invalidSeedError'), + }); + return; + } + + const seedString = enteredAllSeeds.join(' '); + const alreadyUsedSeed = custodyAccountsList.find( + account => account?.mnemoinc?.toLowerCase() === seedString, + ); + + if (alreadyUsedSeed) { + setIsCreatingAccount(false); + navigate.navigate('ErrorScreen', { + errorMessage: t( + 'settings.accountComponents.createAccountPage.alreadyUsingSeedError', + ), + }); + return; + } + + const response = await createImportedAccount( + accountInformation.name, + seedString, + ); + if (!response.didWork) { + setIsCreatingAccount(false); + navigate.navigate('ErrorScreen', { + errorMessage: response.error, + }); + return; + } } + setIsCreatingAccount(false); navigate.goBack(); } catch (err) { console.log('Create custody account error', err); + setIsCreatingAccount(false); navigate.navigate('ErrorScreen', { errorMessage: err.message }); } }; @@ -334,36 +303,15 @@ export default function CreateCustodyAccountPage(props) { { - if (currentWalletMnemoinc === selectedAccount?.mnemoinc) { - navigate.navigate('ErrorScreen', { - errorMessage: t( - 'settings.accountComponents.createAccountPage.cannotDeleteActiveAccountError', - ), - }); - return; - } - navigate.navigate('ConfirmActionPage', { - confirmMessage: t( - 'settings.accountComponents.createAccountPage.deleteAccountConfirmation', - ), - confirmFunction: () => removeAccount(selectedAccount), - cancelFunction: () => {}, - }); - }} /> - {nameIsAlreadyUsed && ( - { - navigate.navigate('InformationPopup', { - textContent: t( - 'settings.accountComponents.createAccountPage.nameTakenError', - ), - buttonText: t('constants.understandText'), - }); - }} - > - - - )} - {!selectedAccount && ( + {accountType === 'imported' && ( <> - setInputedKey(INITIAL_KEY_STATE)} - buttonStyles={{ - flex: 1, - minWidth: 150, - backgroundColor: theme ? backgroundOffset : COLORS.primary, - }} - textStyles={{ color: COLORS.darkModeText }} - textContent={t('constants.restore')} - /> - - {inputedKey === INITIAL_KEY_STATE && ( - { const response = await getClipboardText(); if (!response.didWork) throw new Error(t(response.reason)); @@ -512,12 +412,9 @@ export default function CreateCustodyAccountPage(props) { }); setInputedKey(newKeys); }} - buttonStyles={{ - marginTop: 10, - }} textContent={t('constants.paste')} /> - )} + )} @@ -531,18 +428,13 @@ export default function CreateCustodyAccountPage(props) { ...CENTER, opacity: !accountInformation.name || - nameIsAlreadyUsed || - enteredAllSeeds.length !== 12 || - selectedAccount?.name?.toLowerCase() === - accountInformation?.name?.toLowerCase() + (accountType !== 'derived' && enteredAllSeeds.length !== 12) ? HIDDEN_OPACITY : 1, }} - textContent={ - selectedAccount - ? t('settings.accountComponents.createAccountPage.updateTitle') - : t('settings.accountComponents.createAccountPage.createTitle') - } + textContent={t( + 'settings.accountComponents.createAccountPage.createTitle', + )} actionFunction={handleCreateAccount} /> )} diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountName.js b/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountName.js new file mode 100644 index 00000000..d8828997 --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountName.js @@ -0,0 +1,104 @@ +import { + CustomKeyboardAvoidingView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import { ScrollView, StyleSheet } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { COLORS, WINDOWWIDTH } from '../../../../../constants/theme'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import { useTranslation } from 'react-i18next'; +import { useCallback, useState } from 'react'; +import CustomSearchInput from '../../../../../functions/CustomElements/searchInput'; +import CustomButton from '../../../../../functions/CustomElements/button'; +import { CENTER } from '../../../../../constants'; +import { keyboardGoBack } from '../../../../../functions/customNavigation'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; +import GetThemeColors from '../../../../../hooks/themeColors'; + +export default function EditAccountName(props) { + const selectedAccount = props?.route?.params?.account; + const maxLength = 50; + const { updateAccount } = useActiveCustodyAccount(); + const { t } = useTranslation(); + const { theme, darkModeType } = useGlobalThemeContext(); + const [isKeyboardActive, setIsKeyboardActive] = useState(false); + const [accountName, setAccountName] = useState(selectedAccount.name || ''); + const { textColor } = GetThemeColors(); + + const navigate = useNavigation(); + + const handleNameUpage = useCallback(async () => { + if (!canSave) { + navigate.goBack(); + return; + } + await updateAccount({ + ...selectedAccount, + name: + accountName || + t('accountCard.fallbackAccountName', { + index: selectedAccount.derivationIndex, + }), + }); + keyboardGoBack(navigate); + }, [selectedAccount, accountName]); + + const canSave = selectedAccount.name !== accountName; + + const isOverLimit = accountName.length >= maxLength; + const characterCountColor = isOverLimit + ? theme && darkModeType + ? textColor + : COLORS.cancelRed + : textColor; + + return ( + + + + + setIsKeyboardActive(true)} + onBlurFunction={() => setIsKeyboardActive(false)} + maxLength={maxLength} + /> + + + + + ); +} +const styles = StyleSheet.create({ + scrollContainer: { + paddingTop: 10, + width: WINDOWWIDTH, + ...CENTER, + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountPage.js new file mode 100644 index 00000000..0e540655 --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/editAccountPage.js @@ -0,0 +1,326 @@ +import { + GlobalThemeView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { COLORS, SIZES, WINDOWWIDTH } from '../../../../../constants/theme'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import GetThemeColors from '../../../../../hooks/themeColors'; +import { useTranslation } from 'react-i18next'; +import { useCallback } from 'react'; +import AccountProfileImage from '../../accounts/accountProfileImage'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; +import { useGlobalContextProvider } from '../../../../../../context-store/context'; +import { useToast } from '../../../../../../context-store/toastManager'; + +export default function EditAccountPage(props) { + const { showToast } = useToast(); + const selectedAccount = props?.route?.params?.account; + const fromPage = props?.route?.params?.from; + const { getAccountMnemonic, custodyAccounts, activeAccount } = + useActiveCustodyAccount(); + const { toggleMasterInfoObject, masterInfoObject } = + useGlobalContextProvider(); + const { backgroundOffset, backgroundColor, textColor } = GetThemeColors(); + const { theme, darkModeType } = useGlobalThemeContext(); + const { t } = useTranslation(); + + const accountInformation = + custodyAccounts?.find(item => item.uuid === selectedAccount.uuid) || + selectedAccount || + {}; + const pinnedAccountUUIDs = masterInfoObject?.pinnedAccounts || []; + + const isPinned = pinnedAccountUUIDs.includes( + accountInformation.uuid || accountInformation.name, + ); + const isActive = activeAccount.uuid === accountInformation.uuid; + + const navigate = useNavigation(); + + const handleProfileImage = () => { + navigate.navigate('EmojiAvatarSelector', { account: accountInformation }); + }; + + const handleNavigateView = useCallback(async () => { + const mnemonic = await getAccountMnemonic(selectedAccount); + navigate.navigate('SeedPhraseWarning', { + mnemonic: mnemonic, + extraData: { canViewQrCode: false }, + fromPage: 'accounts', + }); + }, [selectedAccount]); + + const handleEditName = useCallback(async () => { + navigate.navigate('EditAccountName', { + account: accountInformation, + }); + }, [accountInformation]); + + const handlePinToggle = useCallback(() => { + const accountId = accountInformation.uuid || accountInformation.name; + const currentPins = masterInfoObject.pinnedAccounts || []; + const isPinned = currentPins.includes(accountId); + + if (isPinned) { + toggleMasterInfoObject({ + pinnedAccounts: currentPins.filter(id => id !== accountId), + }); + } else { + if (currentPins.length >= 2) { + showToast({ + type: 'error', + title: t('settings.hub.maxPinsReached'), + }); + return; + } + toggleMasterInfoObject({ + pinnedAccounts: [...currentPins, accountId], + }); + } + }, [ + masterInfoObject.pinnedAccounts, + toggleMasterInfoObject, + showToast, + t, + accountInformation, + ]); + + return ( + + + + + + + + + + + + + {/* Account Name */} + + + + + + + + + + + {/* Show Recovery Phrase */} + + + + + + + {/* Pin Contact */} + + {/* Account Name */} + + + + + + + + + + + {/* Danger Zone */} + + { + if (isActive) { + navigate.navigate('ErrorScreen', { + errorMessage: t( + 'settings.accountComponents.editAccountPage.activeAccountError', + ), + }); + return; + } + navigate.navigate('RemoveAccountPage', { + account: accountInformation, + from: fromPage, + }); + }} + > + + + + + + ); +} +const styles = StyleSheet.create({ + avatarContainer: { + marginBottom: 25, + alignSelf: 'center', + }, + + avatar: { + width: 120, + height: 120, + borderRadius: 60, + alignItems: 'center', + justifyContent: 'center', + }, + + editBadge: { + position: 'absolute', + bottom: 6, + right: 6, + width: 28, + height: 28, + borderRadius: 14, + alignItems: 'center', + justifyContent: 'center', + }, + + card: { + alignSelf: 'center', + width: WINDOWWIDTH, + borderRadius: 16, + marginBottom: 16, + overflow: 'hidden', + }, + + row: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingVertical: 18, + paddingHorizontal: 16, + gap: 15, + }, + + rowLabel: { + includeFontPadding: false, + }, + + rowRight: { + width: '100%', + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + }, + + rowValue: { + fontSize: SIZES.small, + opacity: 0.6, + flexShrink: 1, + includeFontPadding: false, + }, + + divider: { + height: 2, + marginLeft: 16, + }, + + dangerRow: { + justifyContent: 'center', + }, + + dangerText: { + color: COLORS.cancelRed, + includeFontPadding: false, + }, + pinButton: { + height: 35, + width: 35, + borderRadius: 12, + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/removeAccountPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/removeAccountPage.js new file mode 100644 index 00000000..1bffb432 --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/removeAccountPage.js @@ -0,0 +1,170 @@ +import { + GlobalThemeView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { COLORS, SIZES } from '../../../../../constants/theme'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import GetThemeColors from '../../../../../hooks/themeColors'; +import { useTranslation } from 'react-i18next'; +import { useCallback } from 'react'; +import CustomButton from '../../../../../functions/CustomElements/button'; +import { CENTER } from '../../../../../constants'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; + +export default function RemoveAccountPage(props) { + const selectedAccount = props?.route?.params?.account; + const fromPage = props?.route?.params?.from; + const { removeAccount } = useActiveCustodyAccount(); + const { backgroundOffset, textColor } = GetThemeColors(); + const { theme, darkModeType } = useGlobalThemeContext(); + const { t } = useTranslation(); + const navigate = useNavigation(); + + const handleRemove = useCallback(async () => { + await removeAccount(selectedAccount); + if (fromPage === 'SettingsContentHome') { + navigate.popTo('SettingsContentHome', { + for: 'Accounts', + }); + } else { + navigate.popTo('SettingsHome'); + } + }, [selectedAccount]); + + const handleCancel = useCallback(() => { + navigate.goBack(); + }, []); + + return ( + + + + + {/* Icon */} + + + + + {/* Title */} + + + {/* Explanation */} + + + + + {/* Buttons */} + + + + + + ); +} + +const styles = StyleSheet.create({ + scrollContainer: { + flexGrow: 1, + ...CENTER, + paddingTop: 40, + }, + contentContainer: { + alignItems: 'center', + paddingHorizontal: 20, + }, + iconContainer: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 30, + }, + title: { + fontSize: SIZES.large, + fontWeight: '500', + textAlign: 'center', + marginBottom: 20, + }, + explanation: { + fontSize: SIZES.medium, + textAlign: 'center', + lineHeight: 22, + opacity: 0.8, + }, + buttonsContainer: { + paddingHorizontal: 20, + gap: 12, + }, + button: { + width: '100%', + }, + cancelButton: { + // Additional styling if needed + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/restoreDerivedAccountPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/restoreDerivedAccountPage.js new file mode 100644 index 00000000..f854924d --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/restoreDerivedAccountPage.js @@ -0,0 +1,308 @@ +import { + FlatList, + StyleSheet, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; +import { + GlobalThemeView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import { CENTER, SIZES } from '../../../../../constants'; +import { COLORS, WINDOWWIDTH } from '../../../../../constants/theme'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; +import GetThemeColors from '../../../../../hooks/themeColors'; +import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import { useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; +import { getRestorableIndices } from '../../../../../functions/accounts/derivedAccounts'; +import { useState } from 'react'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import { useGlobalContextProvider } from '../../../../../../context-store/context'; +import SkeletonPlaceholder from '../../../../../functions/CustomElements/skeletonView'; +// import { +// CustomHalfModal, +// HalfModalFullWidth, +// } from '../../../../../functions/CustomElements/customHalfModal'; + +export default function RestoreDerivedAccountPage() { + const navigate = useNavigation(); + const { t } = useTranslation(); + const { theme, darkModeType } = useGlobalThemeContext(); + const { backgroundOffset, backgroundColor, textColor } = GetThemeColors(); + const { custodyAccounts, restoreDerivedAccount } = useActiveCustodyAccount(); + const { masterInfoObject } = useGlobalContextProvider(); + + const [isRestoring, setIsRestoring] = useState(0); + + const restorableIndices = getRestorableIndices( + custodyAccounts, + masterInfoObject.nextAccountDerivationIndex, + ); + + const handleRestore = async index => { + if (!index) { + navigate.navigate('ErrorScreen', { + errorMessage: t( + 'settings.accountComponents.restoreDerivedAccount.errorMessage', + ), + }); + return; + } + + setIsRestoring(index); + + try { + const result = await restoreDerivedAccount( + t('accountCard.fallbackAccountName', { + index, + }), + index, + ); + + if (result.didWork) { + // Navigate back to show the restored account + navigate.goBack(); + } else { + navigate.navigate('ErrorScreen', { + errorMessage: + result.error || + t('settings.accountComponents.restoreDerivedAccount.errorMessage'), + }); + } + } catch (err) { + console.log('Restore error', err); + navigate.navigate('ErrorScreen', { + errorMessage: t( + 'settings.accountComponents.restoreDerivedAccount.errorMessage', + ), + }); + } finally { + setIsRestoring(0); + } + }; + + const renderAccountCard = ({ item: index }) => { + if (index === isRestoring) { + return ( + + + + + + + + + ); + } + + return ( + handleRestore(index)} + style={[ + styles.accountCard, + { + backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + }, + ]} + > + + + + + + + + + ); + }; + + const renderEmptyState = () => ( + + + + + + + + ); + + return ( + + + + {restorableIndices.length === 0 ? ( + renderEmptyState() + ) : ( + `restorable-${item}`} + contentContainerStyle={styles.listContainer} + showsVerticalScrollIndicator={false} + /> + )} + + ); +} + +const styles = StyleSheet.create({ + listContainer: { + paddingTop: 20, + paddingHorizontal: 16, + paddingBottom: 40, + }, + accountCard: { + paddingVertical: 10, + paddingHorizontal: 15, + marginVertical: 8, + borderRadius: 8, + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 22.5, + alignItems: 'center', + justifyContent: 'center', + }, + accountInfo: { + flex: 1, + }, + accountTitle: { + fontWeight: '500', + fontSize: SIZES.medium, + includeFontPadding: false, + }, + accountSubtitle: { + fontSize: SIZES.small, + opacity: 0.7, + includeFontPadding: false, + marginTop: 2, + }, + emptyStateContainer: { + flex: 1, + ...CENTER, + paddingTop: 100, + paddingHorizontal: 32, + alignItems: 'center', + }, + emptyStateTitle: { + fontSize: SIZES.large, + fontWeight: '500', + marginTop: 20, + includeFontPadding: false, + textAlign: 'center', + }, + emptyStateMessage: { + fontSize: SIZES.medium, + opacity: 0.7, + marginTop: 8, + includeFontPadding: false, + textAlign: 'center', + }, + modalContent: { + padding: 20, + }, + modalTitle: { + fontSize: SIZES.large, + fontWeight: '600', + includeFontPadding: false, + marginBottom: 20, + textAlign: 'center', + }, + inputContainer: { + borderRadius: 8, + padding: 16, + marginBottom: 12, + }, + input: { + fontSize: SIZES.medium, + includeFontPadding: false, + }, + errorText: { + fontSize: SIZES.small, + color: COLORS.cancelRed, + includeFontPadding: false, + marginBottom: 12, + textAlign: 'center', + }, + buttonContainer: { + flexDirection: 'row', + gap: 12, + marginTop: 20, + }, + // Skeleton styles + skeletonContainer: { + flexDirection: 'row', + alignItems: 'center', + width: 250, + height: 40, + }, + skeletonBadge: { + width: 40, + height: 40, + borderRadius: 20, + marginRight: 12, + }, + skeletonText: { + width: 150, + height: 20, + borderRadius: 4, + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/selectCreateAccountType.js b/app/components/admin/homeComponents/settingsContent/accountComponents/selectCreateAccountType.js new file mode 100644 index 00000000..ab767331 --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/selectCreateAccountType.js @@ -0,0 +1,201 @@ +import { + GlobalThemeView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { CENTER, SIZES } from '../../../../../constants'; +import { + COLORS, + HIDDEN_OPACITY, + WINDOWWIDTH, +} from '../../../../../constants/theme'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; +import GetThemeColors from '../../../../../hooks/themeColors'; +import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import { useNavigation } from '@react-navigation/native'; +import { useTranslation } from 'react-i18next'; +import { getRestorableIndices } from '../../../../../functions/accounts/derivedAccounts'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import { useGlobalContextProvider } from '../../../../../../context-store/context'; + +export default function SelectCreateAccountType() { + const navigate = useNavigation(); + const { t } = useTranslation(); + const { theme } = useGlobalThemeContext(); + const { custodyAccounts } = useActiveCustodyAccount(); + const { masterInfoObject } = useGlobalContextProvider(); + + const restorableIndices = getRestorableIndices( + custodyAccounts, + masterInfoObject.nextAccountDerivationIndex, + ); + + const { backgroundOffset, backgroundColor } = GetThemeColors(); + + return ( + + + + + {/* Derived Account Option */} + + navigate.navigate('CreateCustodyAccount', { + accountType: 'derived', + }) + } + style={[ + styles.rowContainer, + { + backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + }, + ]} + > + + + + + + + + + + {/* Imported Account Option */} + + navigate.navigate('CreateCustodyAccount', { + accountType: 'imported', + }) + } + style={[ + styles.rowContainer, + { + backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + }, + ]} + > + + + + + + + + + {/* Restore already created Account */} + { + if (!restorableIndices.length) return; + navigate.navigate('RestoreDerivedAccount'); + }} + style={[ + styles.rowContainer, + { + backgroundColor: theme ? backgroundOffset : COLORS.darkModeText, + opacity: restorableIndices.length ? 1 : HIDDEN_OPACITY, + }, + ]} + > + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + innerContainer: { + flex: 1, + width: WINDOWWIDTH, + ...CENTER, + marginTop: 20, + }, + rowContainer: { + padding: 12, + borderRadius: 8, + marginBottom: 12, + flexDirection: 'row', + alignItems: 'center', + gap: 12, + }, + iconContainer: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + }, + textContainer: { flex: 1 }, + titleText: { + fontWeight: '500', + includeFontPadding: false, + }, + descText: { + fontSize: SIZES.small, + opacity: 0.7, + includeFontPadding: false, + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/selectProfileImage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/selectProfileImage.js new file mode 100644 index 00000000..16c3a54e --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/selectProfileImage.js @@ -0,0 +1,274 @@ +import React, { useState, useMemo, useCallback, useRef, memo } from 'react'; +import { View, TouchableOpacity, StyleSheet, SectionList } from 'react-native'; +import { EMOJI_CATEGORIES } from '../../../../../functions/accounts/handleEmoji'; +import { + CustomKeyboardAvoidingView, + ThemeText, +} from '../../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; +import GetThemeColors from '../../../../../hooks/themeColors'; +import CustomSearchInput from '../../../../../functions/CustomElements/searchInput'; +import ThemeIcon from '../../../../../functions/CustomElements/themeIcon'; +import { COLORS, CONTENT_KEYBOARD_OFFSET } from '../../../../../constants'; +import { HIDDEN_OPACITY, SIZES } from '../../../../../constants/theme'; +import CustomButton from '../../../../../functions/CustomElements/button'; +import { useActiveCustodyAccount } from '../../../../../../context-store/activeAccount'; +import { keyboardGoBack } from '../../../../../functions/customNavigation'; +import { useNavigation } from '@react-navigation/native'; +import AccountProfileImage from '../../accounts/accountProfileImage'; +import { useTranslation } from 'react-i18next'; + +const EmojiRow = memo(({ item, onEmojiSelect }) => ( + + {item.map((emoji, index) => ( + onEmojiSelect(emoji.emoji)} + activeOpacity={0.7} + > + + + ))} + +)); + +const SectionHeader = memo(({ title, backgroundColor }) => ( + + + +)); + +const EmojiGrid = memo(({ sections, onEmojiSelect, backgroundColor }) => { + const renderEmojiRow = useCallback( + ({ item }) => , + [onEmojiSelect], + ); + + const renderSectionHeader = useCallback( + ({ section }) => ( + + ), + [backgroundColor], + ); + + return ( + `row-${index}`} + renderItem={renderEmojiRow} + renderSectionHeader={renderSectionHeader} + stickySectionHeadersEnabled={true} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContent} + initialNumToRender={10} + maxToRenderPerBatch={10} + windowSize={5} + /> + ); +}); + +const AvatarPreview = memo( + ({ selectedEmoji, backgroundOffset, onClear, selectedAccount }) => ( + + + + {!!selectedEmoji && ( + + + + )} + + + ), +); + +export default function EmojiAvatarSelector(props) { + const navigate = useNavigation(); + const selectedAccount = props?.route?.params?.account; + const { updateAccount } = useActiveCustodyAccount(); + const { t } = useTranslation(); + + const [selectedEmoji, setSelectedEmoji] = useState( + selectedAccount.profileEmoji || '', + ); + const [isKeyboardActive, setIsKeyboardActive] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + + const { backgroundOffset, backgroundColor } = GetThemeColors(); + + const handleEmojiSelect = useCallback(emoji => { + setSelectedEmoji(emoji); + }, []); + + const handleClear = useCallback(() => { + setSelectedEmoji(''); + }, []); + + const handleSave = useCallback(() => { + if (selectedEmoji !== selectedAccount.profileEmoji) { + updateAccount({ ...selectedAccount, profileEmoji: selectedEmoji }); + keyboardGoBack(navigate); + } else { + navigate.goBack(); + } + }, [selectedEmoji, updateAccount, selectedAccount, navigate]); + + // Only recomputes on searchQuery change + const filteredSections = useMemo(() => { + if (!searchQuery.trim()) return EMOJI_CATEGORIES; + + const query = searchQuery.toLowerCase(); + return EMOJI_CATEGORIES.map(section => ({ + ...section, + data: section.data.filter( + emoji => + emoji.name.toLowerCase().includes(query) || + emoji.shortName.toLowerCase().includes(query), + ), + })).filter(section => section.data.length > 0); + }, [searchQuery]); + + // Chunking into rows of 7 — only recomputes on searchQuery change + const sectionsWithRows = useMemo(() => { + return filteredSections.map(section => { + const rows = []; + for (let i = 0; i < section.data.length; i += 7) { + rows.push(section.data.slice(i, i + 7)); + } + return { title: section.title, data: rows }; + }); + }, [filteredSections]); + + const saveLabel = + selectedEmoji === selectedAccount.profileEmoji + ? t('constants.back') + : t('settings.accountComponents.selectProfileImage.saveButton'); + + return ( + + + + + + setIsKeyboardActive(true)} + onBlurFunction={() => setIsKeyboardActive(false)} + /> + + + + + + ); +} + +const styles = StyleSheet.create({ + previewContainer: { + alignItems: 'center', + marginTop: 10, + marginBottom: 32, + }, + previewCircle: { + width: 120, + height: 120, + borderRadius: 100, + alignItems: 'center', + justifyContent: 'center', + position: 'relative', + }, + previewEmoji: { + fontSize: 60, + }, + clearButton: { + position: 'absolute', + bottom: 8, + right: 8, + width: 30, + height: 30, + borderRadius: 18, + backgroundColor: COLORS.darkModeText, + alignItems: 'center', + justifyContent: 'center', + }, + searchContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + listContent: { + paddingBottom: 100, + }, + sectionHeader: { + paddingBottom: 12, + paddingTop: 10, + }, + sectionTitle: { + fontWeight: '500', + textTransform: 'uppercase', + letterSpacing: 0.5, + opacity: HIDDEN_OPACITY, + }, + emojiRow: { + flexDirection: 'row', + marginBottom: 8, + flexWrap: 'wrap', + }, + emojiButton: { + alignItems: 'center', + justifyContent: 'center', + flexGrow: 1, + }, + emojiText: { + fontSize: SIZES.xxLarge, + }, + saveButton: { + alignItems: 'center', + alignSelf: 'center', + marginTop: CONTENT_KEYBOARD_OFFSET, + }, +}); diff --git a/app/components/admin/homeComponents/settingsContent/accountComponents/viewAccountPage.js b/app/components/admin/homeComponents/settingsContent/accountComponents/viewAccountPage.js index cbda0129..915b4c04 100644 --- a/app/components/admin/homeComponents/settingsContent/accountComponents/viewAccountPage.js +++ b/app/components/admin/homeComponents/settingsContent/accountComponents/viewAccountPage.js @@ -1,44 +1,45 @@ -import {useNavigation} from '@react-navigation/native'; -import {CENTER, COLORS, SIZES} from '../../../../../constants'; +import { useNavigation } from '@react-navigation/native'; +import { CENTER, COLORS, SIZES } from '../../../../../constants'; import { GlobalThemeView, ThemeText, } from '../../../../../functions/CustomElements'; -import {useState} from 'react'; -import {ScrollView, StyleSheet, View} from 'react-native'; -import {useGlobalThemeContext} from '../../../../../../context-store/theme'; -import {useToast} from '../../../../../../context-store/toastManager'; +import { useState } from 'react'; +import { ScrollView, StyleSheet, View } from 'react-native'; +import { useGlobalThemeContext } from '../../../../../../context-store/theme'; +import { useToast } from '../../../../../../context-store/toastManager'; import calculateSeedQR from '../seedQR'; -import {INSET_WINDOW_WIDTH} from '../../../../../constants/theme'; -import {useTranslation} from 'react-i18next'; +import { INSET_WINDOW_WIDTH } from '../../../../../constants/theme'; +import { useTranslation } from 'react-i18next'; import QrCodeWrapper from '../../../../../functions/CustomElements/QrWrapper'; -import {KeyContainer} from '../../../../login'; +import { KeyContainer } from '../../../../login'; import CustomButton from '../../../../../functions/CustomElements/button'; import CustomSettingsTopBar from '../../../../../functions/CustomElements/settingsTopBar'; -import {copyToClipboard} from '../../../../../functions'; +import { copyToClipboard } from '../../../../../functions'; import WordsQrToggle from '../../../../../functions/CustomElements/wordsQrToggle'; -export default function ViewCustodyAccountPage({route}) { - const {showToast} = useToast(); - const {account} = route.params; - const {extraData} = route.params; +export default function ViewCustodyAccountPage({ route }) { + const { showToast } = useToast(); + const { account } = route.params; + const { extraData } = route.params; const mnemoinc = account.mnemoinc; - const {t} = useTranslation(); + const { t } = useTranslation(); const [seedContainerHeight, setSeedContainerHeight] = useState(0); const navigate = useNavigation(); const [selectedDisplayOption, setSelectedDisplayOption] = useState('words'); const canViewQrCode = extraData?.canViewQrCode; const qrValue = calculateSeedQR(mnemoinc); - const {theme, darkModeType} = useGlobalThemeContext(); + const { theme, darkModeType } = useGlobalThemeContext(); return ( + contentContainerStyle={styles.scrollViewStyles} + > @@ -56,7 +58,8 @@ export default function ViewCustodyAccountPage({route}) { height: seedContainerHeight, alignItems: 'center', justifyContent: 'center', - }}> + }} + > ) : ( @@ -64,7 +67,8 @@ export default function ViewCustodyAccountPage({route}) { onLayout={event => { setSeedContainerHeight(event.nativeEvent.layout.height); }} - style={styles.scrollViewContainer}> + style={styles.scrollViewContainer} + > )} @@ -76,7 +80,7 @@ export default function ViewCustodyAccountPage({route}) { navigate.popTo( 'ViewCustodyAccount', { - extraData: {canViewQrCode: true}, + extraData: { canViewQrCode: true }, }, { merge: true, @@ -86,7 +90,7 @@ export default function ViewCustodyAccountPage({route}) { /> copyToClipboard( selectedDisplayOption === 'words' ? mnemoinc : qrValue, diff --git a/app/components/admin/homeComponents/settingsContent/accounts.js b/app/components/admin/homeComponents/settingsContent/accounts.js index b1d2519b..99ea2dbf 100644 --- a/app/components/admin/homeComponents/settingsContent/accounts.js +++ b/app/components/admin/homeComponents/settingsContent/accounts.js @@ -11,7 +11,6 @@ import { COLORS, INSET_WINDOW_WIDTH, MAX_CONTENT_WIDTH, - SIZES, } from '../../../../constants/theme'; import GetThemeColors from '../../../../hooks/themeColors'; import { useGlobalThemeContext } from '../../../../../context-store/theme'; @@ -19,185 +18,9 @@ import { useActiveCustodyAccount } from '../../../../../context-store/activeAcco import CustomSearchInput from '../../../../functions/CustomElements/searchInput'; import { initWallet } from '../../../../functions/initiateWalletConnection'; import { useSparkWallet } from '../../../../../context-store/sparkContext'; -import useCustodyAccountList from '../../../../hooks/useCustodyAccountsList'; import { useTranslation } from 'react-i18next'; import { useWebView } from '../../../../../context-store/webViewContext'; -import Animated, { - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; -import FullLoadingScreen from '../../../../functions/CustomElements/loadingScreen'; -import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; - -const AccountRow = React.memo( - ({ - account, - theme, - darkModeType, - textColor, - currentWalletMnemoinc, - isLoading, - expandedAccount, - onToggleExpand, - onNavigateView, - onNavigateEdit, - onSelectAccount, - t, - }) => { - const isMainWallet = account.name === 'Main Wallet'; - const isNWC = account.name === 'NWC'; - const isSpecialAccount = isMainWallet; - const isActive = currentWalletMnemoinc === account.mnemoinc; - const isAccountLoading = - isLoading.accountBeingLoaded === account.mnemoinc && isLoading.isLoading; - const isExpanded = expandedAccount === account.mnemoinc; - - const expandHeight = useSharedValue(0); - const chevronRotation = useSharedValue(0); - - React.useEffect(() => { - expandHeight.value = withTiming(isExpanded ? 1 : 0, { - stiffness: 300, - }); - chevronRotation.value = withTiming(isExpanded ? 1 : 0, { - duration: 200, - }); - }, [isExpanded]); - - const expandedStyle = useAnimatedStyle(() => ({ - height: expandHeight.value * (50 * (isNWC ? 1 : 2)), - opacity: expandHeight.value, - })); - - const chevronStyle = useAnimatedStyle(() => ({ - transform: [{ rotate: `${chevronRotation.value * 180}deg` }], - })); - - return ( - - - { - if (isActive && !isSpecialAccount) { - onToggleExpand(account.mnemoinc); - } else if (!isActive) { - onSelectAccount(account); - } - }} - > - - - {isActive && ( - - )} - - - {isActive && ( - - )} - - - - - {isActive && !isSpecialAccount && ( - - - - )} - - {!isActive && !isAccountLoading && ( - - )} - - {isAccountLoading && ( - - )} - - - - {!isActive && !isSpecialAccount && ( - { - if (!isSpecialAccount) { - onToggleExpand(account.mnemoinc); - } - }} - > - - - )} - - - {!isSpecialAccount && ( - - onNavigateView(account)} - > - - - - - {!isNWC && ( - onNavigateEdit(account)} - > - - - - )} - - )} - - - - ); - }, -); +import AccountCard from '../accounts/accountCard'; export default function CreateCustodyAccounts() { const navigate = useNavigation(); @@ -207,41 +30,36 @@ export default function CreateCustodyAccounts() { updateAccountCacheOnly, currentWalletMnemoinc, toggleIsUsingNostr, + getAccountMnemonic, + isUsingNostr, + custodyAccountsList, + activeAccount, } = useActiveCustodyAccount(); const { setSparkInformation } = useSparkWallet(); - const { textColor, backgroundColor, backgroundOffset } = GetThemeColors(); + const { backgroundColor, backgroundOffset } = GetThemeColors(); const [searchInput, setSearchInput] = useState(''); - const [expandedAccount, setExpandedAccount] = useState(null); const [isLoading, setIsLoading] = useState({ accountBeingLoaded: '', isLoading: false, }); const { t } = useTranslation(); - const accounts = useCustodyAccountList(); const { sendWebViewRequest } = useWebView(); const filteredAccounts = useMemo(() => { - if (!searchInput.trim()) return accounts; + if (!searchInput.trim()) return custodyAccountsList; const searchTerm = searchInput.toLowerCase(); - return accounts.filter(account => + return custodyAccountsList.filter(account => account.name?.toLowerCase()?.includes(searchTerm), ); - }, [accounts, searchInput]); - - const handleToggleExpand = useCallback(mnemonic => { - setExpandedAccount(prev => (prev === mnemonic ? null : mnemonic)); - }, []); - - const handleNavigateView = useCallback( - account => { - navigate.navigate('ViewCustodyAccount', { account }); - }, - [navigate], - ); + }, [custodyAccountsList, searchInput]); const handleNavigateEdit = useCallback( account => { - navigate.navigate('CreateCustodyAccount', { account }); + if (account.name === 'Main Wallet' || account.name === 'NWC') return; + navigate.navigate('EditAccountPage', { + account, + from: 'SettingsContentHome', + }); }, [navigate], ); @@ -254,12 +72,11 @@ export default function CreateCustodyAccounts() { ); const handleNavigateAddAccount = useCallback(() => { - navigate.navigate('CreateCustodyAccount', {}); + navigate.navigate('SelectCreateAccountType', {}); }, [navigate]); const handleNavigateSwap = useCallback(() => { - console.log(accounts); - if (accounts.length < 2) { + if (custodyAccountsList.length < 2) { navigate.navigate('ErrorScreen', { errorMessage: t('settings.accountComponents.homepage.swapAccountError'), }); @@ -267,23 +84,24 @@ export default function CreateCustodyAccounts() { } navigate.navigate('CustodyAccountPaymentPage'); - }, [navigate, accounts]); + }, [navigate, custodyAccountsList, t]); const handleSelectAccount = useCallback( async account => { - if (currentWalletMnemoinc === account.mnemoinc) return; - - setIsLoading({ - accountBeingLoaded: account.mnemoinc, - isLoading: true, - }); - try { + const accountMnemonic = await getAccountMnemonic(account); + if (currentWalletMnemoinc === accountMnemonic) return; + + setIsLoading({ + accountBeingLoaded: account.uuid || account.name, + isLoading: true, + }); + await new Promise(resolve => setTimeout(resolve, 250)); const initResponse = await initWallet({ setSparkInformation, - mnemonic: account.mnemoinc, + mnemonic: accountMnemonic, sendWebViewRequest, }); @@ -296,10 +114,12 @@ export default function CreateCustodyAccounts() { const isNWC = account.name === 'NWC'; if (isMainWallet || isNWC) { - await updateAccountCacheOnly({ - ...selectedAltAccount[0], - isActive: false, - }); + if (selectedAltAccount[0]) { + await updateAccountCacheOnly({ + ...selectedAltAccount[0], + isActive: false, + }); + } toggleIsUsingNostr(isNWC); } else { await updateAccountCacheOnly({ ...account, isActive: true }); @@ -322,39 +142,33 @@ export default function CreateCustodyAccounts() { toggleIsUsingNostr, handleNavigateError, sendWebViewRequest, + getAccountMnemonic, ], ); const accountElements = useMemo(() => { - return filteredAccounts.map((account, index) => ( - - )); + return filteredAccounts.map((account, index) => { + return ( + handleSelectAccount(account)} + onEdit={() => handleNavigateEdit(account)} + isLoading={ + isLoading.accountBeingLoaded === (account.uuid || account.name) && + isLoading.isLoading + } + /> + ); + }); }, [ filteredAccounts, - theme, - textColor, - currentWalletMnemoinc, + isUsingNostr, isLoading, - expandedAccount, - handleToggleExpand, - handleNavigateView, handleNavigateEdit, handleSelectAccount, - t, + activeAccount, ]); return ( @@ -370,58 +184,12 @@ export default function CreateCustodyAccounts() { - - - - - - - - - - - - @@ -439,6 +207,35 @@ export default function CreateCustodyAccounts() { )} + + + + + + + + ); } @@ -449,21 +246,14 @@ const styles = StyleSheet.create({ maxWidth: MAX_CONTENT_WIDTH, ...CENTER, paddingTop: 20, + flexGrow: 1, }, listContainer: { width: '100%', paddingTop: 8, }, - actionButtons: { - flexDirection: 'row', - flexWrap: 'wrap', - width: '100%', - gap: 12, - ...CENTER, - paddingBottom: CONTENT_KEYBOARD_OFFSET, - }, + actionButton: { - flex: 1, width: '100%', minWidth: 150, flexDirection: 'row', @@ -473,98 +263,12 @@ const styles = StyleSheet.create({ gap: 8, borderRadius: 8, }, - actionButtonIcon: { - width: 20, - height: 20, - }, actionButtonText: { fontWeight: '500', color: COLORS.darkModeText, includeFontPadding: false, flexShrink: 1, }, - row: { - width: '100%', - paddingVertical: 4, - }, - fullRow: { - width: '100%', - flexDirection: 'row', - alignItems: 'center', - }, - rowTouchable: { - width: '100%', - flexShrink: 1, - marginRight: 10, - paddingVertical: 18, - }, - rowContent: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, - leftSection: { - flexDirection: 'row', - alignItems: 'center', - flex: 1, - gap: 12, - }, - activeDot: { - width: 10, - height: 10, - borderRadius: 5, - }, - accountText: { - fontSize: 16, - includeFontPadding: false, - letterSpacing: 0.2, - }, - activeText: { - fontWeight: '500', - }, - activeTextFlag: { - fontSize: SIZES.small, - }, - rightSection: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: 16, - }, - // expandIcon: { paddingHorizontal: 5 }, - chevron: { - width: 25, - height: 25, - transform: [{ rotate: '-90deg' }], - }, - selectText: { - fontSize: 14, - opacity: 0.7, - includeFontPadding: false, - }, - expanded: { - overflow: 'hidden', - paddingLeft: 18, - }, - expandedAction: { - height: 50, - flexDirection: 'row', - alignItems: 'center', - gap: 14, - }, - actionIcon: { - opacity: 0.9, - }, - actionText: { - includeFontPadding: false, - opacity: 0.8, - }, - divider: { - height: 2, - width: '100%', - opacity: 0.1, - borderRadius: 100, - marginTop: 4, - }, empty: { paddingVertical: 60, alignItems: 'center', diff --git a/app/components/admin/homeComponents/settingsContent/index.js b/app/components/admin/homeComponents/settingsContent/index.js index d67224bb..05c115e7 100644 --- a/app/components/admin/homeComponents/settingsContent/index.js +++ b/app/components/admin/homeComponents/settingsContent/index.js @@ -5,6 +5,7 @@ import NosterWalletConnect from './nwc'; import ConfirmActionPage from './popups/confirmActionPage'; import ResetPage from './resetWallet'; import SeedPhrasePage from './seedPhrasePage'; +import SeedPhraseWarning from './seedPhraseWarning'; import LoginSecurity from './loginSecurity'; import BlitzSocialOptions from './socialOptions'; import PosSettingsPage from './posPath/settings'; @@ -25,6 +26,7 @@ export { ResetPage, ConfirmActionPage, SeedPhrasePage, + SeedPhraseWarning, NosterWalletConnect, LoginSecurity, BlitzSocialOptions, diff --git a/app/components/admin/homeComponents/settingsContent/resetWallet.js b/app/components/admin/homeComponents/settingsContent/resetWallet.js index be5d77ed..efe13025 100644 --- a/app/components/admin/homeComponents/settingsContent/resetWallet.js +++ b/app/components/admin/homeComponents/settingsContent/resetWallet.js @@ -1,4 +1,4 @@ -import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { StyleSheet, TouchableOpacity, View } from 'react-native'; import { CENTER, COLORS, SIZES } from '../../../../constants'; import { useCallback, useMemo, useState } from 'react'; import RNRestart from 'react-native-restart'; @@ -6,33 +6,22 @@ import { ThemeText } from '../../../../functions/CustomElements'; import CustomButton from '../../../../functions/CustomElements/button'; import GetThemeColors from '../../../../hooks/themeColors'; import { useNavigation } from '@react-navigation/native'; -import FormattedSatText from '../../../../functions/CustomElements/satTextDisplay'; -import { useNodeContext } from '../../../../../context-store/nodeContext'; import { useGlobalThemeContext } from '../../../../../context-store/theme'; import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH, } from '../../../../constants/theme'; import { useTranslation } from 'react-i18next'; -import { useAppStatus } from '../../../../../context-store/appStatus'; import factoryResetWallet from '../../../../functions/factoryResetWallet'; -import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext'; import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; export default function ResetPage(props) { const [wantsToReset, setWantsToReset] = useState(false); - const { screenDimensions } = useAppStatus(); - const { totalSatValue } = useUserBalanceContext(); const { theme, darkModeType } = useGlobalThemeContext(); - const { liquidNodeInformation } = useNodeContext(); - const [contentHeight, setContentHeight] = useState(0); const { backgroundOffset, textColor } = GetThemeColors(); const navigate = useNavigation(); const { t } = useTranslation(); - const backgroundColor = useMemo(() => { - return theme ? backgroundOffset : COLORS.darkModeText; - }, [theme, backgroundOffset]); const checkBackground = useMemo(() => { return theme && darkModeType ? COLORS.darkModeText : COLORS.primary; }, [theme, backgroundOffset]); @@ -40,8 +29,6 @@ export default function ResetPage(props) { return theme && darkModeType ? COLORS.lightModeText : COLORS.darkModeText; }, [theme, backgroundOffset]); - const isDoomsday = props.isDoomsday; - const handleSelectedItems = useCallback(() => { setWantsToReset(prev => !prev); }, []); @@ -68,177 +55,129 @@ export default function ResetPage(props) { }, [wantsToReset]); return ( - screenDimensions.height ? 0 : 1, - width: INSET_WINDOW_WIDTH, - ...CENTER, - paddingTop: 24, - }} - > + screenDimensions.height ? 0 : 1, - gap: 16, - }} - onLayout={e => { - if (!e.nativeEvent.layout.height) return; - setContentHeight(e.nativeEvent.layout.height); - }} + style={[ + styles.iconContainer, + { + backgroundColor: + theme && darkModeType ? COLORS.darkModeText : COLORS.primary, + }, + ]} > - - - + + + + - - - - {/* Options */} - - - - {wantsToReset && ( - - )} - - - - - - - {!isDoomsday && ( - - + {wantsToReset && ( + + )} - )} - - + + - + + + ); } const styles = StyleSheet.create({ - warningHeader: { - fontSize: SIZES.large, - fontWeight: '600', + resetContainer: { + flex: 1, + width: INSET_WINDOW_WIDTH, + ...CENTER, + alignItems: 'center', + }, + iconContainer: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 30, + marginTop: 'auto', + }, + title: { + fontSize: SIZES.xLarge, + fontWeight: '500', textAlign: 'center', - letterSpacing: 0.3, - }, - contentCard: { - width: '100%', - padding: 20, - borderRadius: 8, - }, - sectionTitle: { - fontSize: SIZES.medium, - fontWeight: '600', - marginBottom: 8, + marginBottom: 30, }, descriptionText: { - fontSize: SIZES.small, opacity: 0.7, - lineHeight: 20, marginBottom: 24, + textAlign: 'center', }, optionsContainer: { + width: '100%', gap: 16, + marginTop: 'auto', + marginBottom: 5, }, optionRow: { flexDirection: 'row', alignItems: 'center', }, checkbox: { - width: 24, - height: 24, - borderRadius: 6, + width: 20, + height: 20, borderWidth: 2, - marginRight: 12, + borderRadius: 3, + marginRight: 10, alignItems: 'center', justifyContent: 'center', }, - optionLabel: { - flex: 1, - fontSize: SIZES.medium, - }, - balanceCard: { - width: '100%', - padding: 24, - borderRadius: 12, - alignItems: 'center', - }, - balanceLabel: { + checkboxText: { fontSize: SIZES.small, - opacity: 0.7, - marginBottom: 5, - textAlign: 'center', + flex: 1, + includeFontPadding: false, }, - balanceAmount: { - fontSize: SIZES.xLarge, + buttonsContainer: { + paddingHorizontal: 20, + gap: 12, }, + button: {}, }); diff --git a/app/components/admin/homeComponents/settingsContent/seedPhrasePage.js b/app/components/admin/homeComponents/settingsContent/seedPhrasePage.js index 86cdc9e3..13d8ba17 100644 --- a/app/components/admin/homeComponents/settingsContent/seedPhrasePage.js +++ b/app/components/admin/homeComponents/settingsContent/seedPhrasePage.js @@ -1,7 +1,7 @@ import { ScrollView, StyleSheet, View } from 'react-native'; import { KeyContainer } from '../../../login'; -import { useEffect, useRef, useState } from 'react'; -import { COLORS, SIZES, SHADOWS, CENTER } from '../../../../constants'; +import { useState } from 'react'; +import { COLORS, SIZES, CENTER } from '../../../../constants'; import { useNavigation } from '@react-navigation/native'; import { ThemeText } from '../../../../functions/CustomElements'; import CustomButton from '../../../../functions/CustomElements/button'; @@ -15,24 +15,15 @@ import calculateSeedQR from './seedQR'; import { copyToClipboard } from '../../../../functions'; import { useToast } from '../../../../../context-store/toastManager'; import WordsQrToggle from '../../../../functions/CustomElements/wordsQrToggle'; -import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, -} from 'react-native-reanimated'; -import { useAppStatus } from '../../../../../context-store/appStatus'; -import { useGlobalContextProvider } from '../../../../../context-store/context'; - -export default function SeedPhrasePage({ extraData }) { - const { toggleMasterInfoObject, masterInfoObject } = - useGlobalContextProvider(); +export default function SeedPhrasePage({ extraData, route }) { const { showToast } = useToast(); - const fadeAnim = useSharedValue(0); - const { screenDimensions } = useAppStatus(); - const { accountMnemoinc } = useKeysContext(); - const isInitialRender = useRef(true); - const mnemonic = accountMnemoinc.split(' '); - const [showSeed, setShowSeed] = useState(false); + const { accountMnemoinc: contextMnemonic } = useKeysContext(); + + const paramMnemonic = + extraData?.mnemonic || route?.params?.extraData?.mnemonic; + const mnemonicString = paramMnemonic || contextMnemonic; + + const mnemonic = mnemonicString.split(' '); const navigate = useNavigation(); const { backgroundColor, backgroundOffset } = GetThemeColors(); const { theme, darkModeType } = useGlobalThemeContext(); @@ -40,28 +31,7 @@ export default function SeedPhrasePage({ extraData }) { const [seedContainerHeight, setSeedContainerHeight] = useState(); const [selectedDisplayOption, setSelectedDisplayOption] = useState('words'); const canViewQrCode = extraData?.canViewQrCode; - const qrValue = calculateSeedQR(accountMnemoinc); - - useEffect(() => { - if (isInitialRender.current) { - isInitialRender.current = false; - return; - } - if (showSeed) { - fadeout(); - } - }, [showSeed]); - - const animatedStyle = useAnimatedStyle(() => ({ - transform: [{ translateY: fadeAnim.value }], - backgroundColor: backgroundColor, - })); - - function fadeout() { - fadeAnim.value = withTiming(screenDimensions.height * 2, { - duration: 500, - }); - } + const qrValue = calculateSeedQR(mnemonicString); return ( @@ -78,7 +48,7 @@ export default function SeedPhrasePage({ extraData }) { color: theme && darkModeType ? COLORS.darkModeText : COLORS.cancelRed, marginBottom: 50, - fontSize: SIZES.large, + textAlign: 'center', }} content={t('settings.seedPhrase.headerDesc')} @@ -109,8 +79,8 @@ export default function SeedPhrasePage({ extraData }) { canViewQrCode={canViewQrCode} qrNavigateFunc={() => navigate.popTo('SettingsContentHome', { - for: 'Backup wallet', - extraData: { canViewQrCode: true }, + for: 'show seed phrase', + extraData: { ...extraData, canViewQrCode: true }, }) } /> @@ -118,42 +88,13 @@ export default function SeedPhrasePage({ extraData }) { buttonStyles={{ marginTop: 10 }} actionFunction={() => copyToClipboard( - selectedDisplayOption === 'words' ? accountMnemoinc : qrValue, + selectedDisplayOption === 'words' ? mnemonicString : qrValue, showToast, ) } textContent={t('constants.copy')} /> - - - - - - { - if (!masterInfoObject.didViewSeedPhrase) - toggleMasterInfoObject({ didViewSeedPhrase: true }); - setShowSeed(true); - }} - /> - - - - ); } @@ -162,37 +103,11 @@ const styles = StyleSheet.create({ globalContainer: { flex: 1, }, - headerPhrase: { marginBottom: 15, fontSize: SIZES.xLarge, textAlign: 'center', }, - - confirmPopup: { - width: '100%', - height: '100%', - position: 'absolute', - top: 0, - left: 0, - alignItems: 'center', - }, - confirmationContainer: { - flexDirection: 'row', - marginTop: 50, - width: '100%', - justifyContent: 'center', - }, - confirmPopupInnerContainer: { - flex: 1, - width: INSET_WINDOW_WIDTH, - alignItems: 'center', - justifyContent: 'center', - }, - confirmPopupTitle: { - fontSize: SIZES.large, - textAlign: 'center', - }, scrollViewContainer: {}, scrollViewStyles: { width: INSET_WINDOW_WIDTH, @@ -201,54 +116,4 @@ const styles = StyleSheet.create({ paddingBottom: 10, alignItems: 'center', }, - confirmBTN: { - flex: 1, - maxWidth: '45%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 5, - ...SHADOWS.small, - }, - confirmBTNText: { - color: 'white', - paddingVertical: 10, - }, - - // slider contianer - sliderContainer: { - width: 200, - paddingVertical: 5, - borderRadius: 40, - marginTop: 20, - }, - colorSchemeContainer: { - height: 'auto', - flexDirection: 'row', - position: 'relative', - zIndex: 1, - }, - colorSchemeItemContainer: { - width: '50%', - paddingVertical: 8, - alignItems: 'center', - }, - colorSchemeText: { - width: '100%', - includeFontPadding: false, - textAlign: 'center', - flexShrink: 1, - paddingHorizontal: 5, - }, - activeSchemeStyle: { - backgroundColor: COLORS.primary, - position: 'absolute', - height: '100%', - width: 95, - top: -3, - left: 0, - - zIndex: -1, - borderRadius: 30, - }, }); diff --git a/app/components/admin/homeComponents/settingsContent/seedPhraseWarning.js b/app/components/admin/homeComponents/settingsContent/seedPhraseWarning.js new file mode 100644 index 00000000..ead9c9d8 --- /dev/null +++ b/app/components/admin/homeComponents/settingsContent/seedPhraseWarning.js @@ -0,0 +1,284 @@ +import { + GlobalThemeView, + ThemeText, +} from '../../../../functions/CustomElements'; +import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar'; +import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { + COLORS, + INSET_WINDOW_WIDTH, + SIZES, + WINDOWWIDTH, +} from '../../../../constants/theme'; +import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { useTranslation } from 'react-i18next'; +import { useCallback, useState } from 'react'; +import CustomButton from '../../../../functions/CustomElements/button'; +import { CENTER, CONTENT_KEYBOARD_OFFSET } from '../../../../constants'; +import { useGlobalThemeContext } from '../../../../../context-store/theme'; + +export default function SeedPhraseWarning(props) { + const routeMnemonic = props?.route?.params?.mnemonic || props?.mnemonic; + const routeExtraData = props?.route?.params?.extraData || props?.extraData; + const fromPage = props?.route?.params?.fromPage || props?.fromPage; + + const { t } = useTranslation(); + const [termsAccepted, setTermsAccepted] = useState(false); + const { backgroundOffset, textColor } = GetThemeColors(); + const { theme, darkModeType } = useGlobalThemeContext(); + const navigate = useNavigation(); + + const toggleTermsAcceptance = useCallback(() => { + setTermsAccepted(!termsAccepted); + }, [termsAccepted]); + + const handleContinue = useCallback(() => { + if (!termsAccepted) { + return; + } + + const extraData = { + ...routeExtraData, + }; + + if (routeMnemonic) { + extraData.mnemonic = routeMnemonic; + } + + navigate.replace('SettingsContentHome', { + for: 'show seed phrase', + extraData, + }); + }, [termsAccepted, routeMnemonic, routeExtraData]); + + const warningPoints = [ + { + icon: 'Lock', + text: t('settings.seedPhrase.warning.point1'), + }, + { + icon: 'EyeOff', + text: t('settings.seedPhrase.warning.point2'), + }, + { + icon: 'Info', + text: t('settings.seedPhrase.warning.point3'), + }, + ]; + + const WarningContent = useCallback(() => { + return ( + + + + {/* Icon */} + + + + + {/* Title */} + + + {/* Warning Points */} + + {warningPoints.map((point, index) => ( + + + + + + + + + ))} + + + + + {/* Checkbox */} + + + {termsAccepted && ( + + )} + + + + + {/* Continue Button */} + + + ); + }, [ + warningPoints, + termsAccepted, + toggleTermsAcceptance, + handleContinue, + textColor, + theme, + darkModeType, + ]); + + if (fromPage === 'settings') { + return ; + } + + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + scrollContainer: { + flexGrow: 1, + ...CENTER, + paddingTop: 20, + width: INSET_WINDOW_WIDTH, + justifyContent: 'center', + }, + contentContainer: { + alignItems: 'center', + paddingBottom: 20, + }, + iconContainer: { + width: 80, + height: 80, + borderRadius: 40, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 30, + }, + title: { + fontSize: SIZES.xLarge, + fontWeight: '500', + textAlign: 'center', + marginBottom: 30, + }, + warningPointsContainer: { + width: '100%', + gap: 20, + }, + warningPoint: { + flexDirection: 'row', + alignItems: 'flex-start', + gap: 10, + }, + warningIconContainer: { + paddingTop: 2, + }, + warningText: { + flex: 1, + fontSize: SIZES.medium, + includeFontPadding: false, + }, + checkboxContainer: { + paddingTop: CONTENT_KEYBOARD_OFFSET, + paddingBottom: 15, + paddingLeft: 20, + marginBottom: 5, + flexDirection: 'row', + alignItems: 'center', + }, + checkbox: { + width: 20, + height: 20, + borderWidth: 2, + borderRadius: 3, + marginRight: 10, + alignItems: 'center', + justifyContent: 'center', + }, + + checkboxText: { + fontSize: SIZES.small, + flex: 1, + includeFontPadding: false, + }, + continueButton: { + width: 145, + ...CENTER, + }, +}); diff --git a/app/constants/icons.js b/app/constants/icons.js index b0d2c90f..24f6e769 100644 --- a/app/constants/icons.js +++ b/app/constants/icons.js @@ -21,6 +21,7 @@ import blockstreamLiquid from '../assets/icons/blockstreamLiquid.png'; import dollarIcon from '../assets/icons/dollarIcon.png'; import bitcoinIcon from '../assets/icons/bitcoinIcon.png'; import giftCardIcon from '../assets/icons/giftCardIcon.png'; +import nwcLogo from '../assets/icons/nwcLogo.png'; export default { logoIcon, @@ -59,4 +60,5 @@ export default { dollarIcon, bitcoinIcon, + nwcLogo, }; diff --git a/app/constants/index.js b/app/constants/index.js index a440700e..6cd0a25e 100644 --- a/app/constants/index.js +++ b/app/constants/index.js @@ -35,6 +35,8 @@ const FLASHNET_ERROR_CODE_REGEX = /\bFSAG-\d{4}(?:T\d+)?\b/; const FLASHNET_REFUND_REGEX = /via transfer ([\da-fA-F\-]+)/; +const BASIC_ACCOUNT_NAME_REGEX = /^account\s+\d+$/i; + const NOSTR_NAME_REGEX = /^[a-zA-Z0-9]+$/; const NOSTR_RELAY_URL = 'wss://relay.getalbypro.com/blitz'; @@ -90,8 +92,11 @@ const CUSTOM_TOKEN_CURRENCY_OPTIONS = [{ token: 'USDB', currency: 'USD' }]; const CHATGPT_INPUT_COST = 10 / 1000000; const CHATGPT_OUTPUT_COST = 30 / 1000000; -const STARTING_INDEX_FOR_GIFTS_DERIVE = 1000; -const STARTING_INDEX_FOR_POOLS_DERIVE = 100000; + +const MAX_DERIVED_ACCOUNTS = 1000; // Indices 0-999 for user accounts +const STARTING_INDEX_FOR_GIFTS_DERIVE = 1000; // Indices 1000-99999 for gifts +const MAX_GIFTS = 99000; // Maximum 99000 gifts +const STARTING_INDEX_FOR_POOLS_DERIVE = 100000; // Indices 100000+ for pools (unlimited) const POOL_DEEPLINK_REGEX = /^(?:blitz-wallet:\/\/pools\/|https:\/\/(?:blitz-wallet\.com|blitzwalletapp\.com|blitzwallet\.app)\/pools\/)[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}\/?$/; @@ -193,7 +198,9 @@ export { BLITZ_PAYMENT_DEEP_LINK_SCHEMES, HIDE_IN_APP_PURCHASE_ITEMS, IS_BLITZ_URL_REGEX, + MAX_DERIVED_ACCOUNTS, STARTING_INDEX_FOR_GIFTS_DERIVE, + MAX_GIFTS, GIFT_DEEPLINK_REGEX, GIFT_DERIVE_PATH_CUTOFF, CONTACT_UNIVERSAL_LINK_REGEX, @@ -207,4 +214,5 @@ export { CUSTOM_TOKEN_CURRENCY_OPTIONS, STARTING_INDEX_FOR_POOLS_DERIVE, POOL_DEEPLINK_REGEX, + BASIC_ACCOUNT_NAME_REGEX, }; diff --git a/app/functions/CustomElements/profileSettingsNavigator.js b/app/functions/CustomElements/profileSettingsNavigator.js index f6eaeac5..44ca4cc2 100644 --- a/app/functions/CustomElements/profileSettingsNavigator.js +++ b/app/functions/CustomElements/profileSettingsNavigator.js @@ -2,17 +2,13 @@ import { useNavigation } from '@react-navigation/native'; import { useCallback } from 'react'; import { StyleSheet, TouchableOpacity, View } from 'react-native'; import GetTheemColors from '../../hooks/themeColors'; -import ContactProfileImage from '../../components/admin/homeComponents/contacts/internalComponents/profileImage'; -import { useImageCache } from '../../../context-store/imageCache'; -import { useGlobalContextProvider } from '../../../context-store/context'; -import { useGlobalThemeContext } from '../../../context-store/theme'; import { keyboardNavigate } from '../customNavigation'; +import { useActiveCustodyAccount } from '../../../context-store/activeAccount'; +import AccountProfileImage from '../../components/admin/homeComponents/accounts/accountProfileImage'; export default function ProfileImageSettingsNavigator() { - const { darkModeType, theme } = useGlobalThemeContext(); - const { masterInfoObject } = useGlobalContextProvider(); - const { cache } = useImageCache(); const { backgroundOffset } = GetTheemColors(); const navigate = useNavigation(); + const { activeAccount } = useActiveCustodyAccount(); const goToMyProfile = useCallback(() => { keyboardNavigate(() => navigate.navigate('SettingsHome', {})); @@ -26,12 +22,7 @@ export default function ProfileImageSettingsNavigator() { { backgroundColor: backgroundOffset }, ]} > - + ); diff --git a/app/functions/accounts/derivedAccounts.js b/app/functions/accounts/derivedAccounts.js new file mode 100644 index 00000000..3296aadf --- /dev/null +++ b/app/functions/accounts/derivedAccounts.js @@ -0,0 +1,97 @@ +import { MAX_DERIVED_ACCOUNTS } from '../../constants'; +import { deriveSparkGiftMnemonic } from '../gift/deriveGiftWallet'; + +/** + * Derive account mnemonic from main seed using Spark derivation scheme + * Uses the same derivation path as gifts (m/8797555'/{index}'/0') but with a different index range + * @param {string} mainSeed - Main wallet mnemonic + * @param {number} derivationIndex - Account index (0-999) + * @returns {Promise} Derived mnemonic + */ +export async function deriveAccountMnemonic(mainSeed, derivationIndex) { + // CRITICAL: Validate derivation index is in valid range + if ( + typeof derivationIndex !== 'number' || + derivationIndex < 0 || + derivationIndex >= MAX_DERIVED_ACCOUNTS + ) { + throw new Error( + `Derivation index ${derivationIndex} out of range (0-${ + MAX_DERIVED_ACCOUNTS - 1 + })`, + ); + } + + if (!mainSeed || typeof mainSeed !== 'string') { + throw new Error('Main seed must be a non-empty string'); + } + + // Reuse existing Spark derivation (same path as gifts, different index range) + const result = await deriveSparkGiftMnemonic(mainSeed, derivationIndex); + if (!result.success) { + throw new Error(result.error || 'Failed to derive account'); + } + return result.derivedMnemonic; +} + +/** + * Check if account is derived from main seed + * @param {Object} account - Account object + * @returns {boolean} True if account is derived + */ +export function isAccountDerived(account) { + return ( + account && + typeof account === 'object' && + account.derivationIndex !== undefined && + account.derivationIndex !== null + ); +} + +/** + * Check if account is imported (standalone seed) + * @param {Object} account - Account object + * @returns {boolean} True if account is imported + */ +export function isAccountImported(account) { + return ( + account && + typeof account === 'object' && + account.mnemoinc !== undefined && + account.mnemoinc !== null && + account.derivationIndex === undefined + ); +} + +/** + * Returns array of derivation indices that can be restored (gaps in account sequence) + * @param {Array} custodyAccounts - Current account list + * @param {number} nextAccountDerivationIndex - Highest index + 1 (from masterInfoObject) + * @returns {Array} Available indices for restoration (sorted ascending) + */ +export function getRestorableIndices( + custodyAccounts, + nextAccountDerivationIndex, +) { + try { + const maxIndex = nextAccountDerivationIndex || 3; + const existingIndices = new Set( + custodyAccounts + .filter(acc => acc.accountType === 'derived') + .map(acc => acc.derivationIndex) + .filter(idx => typeof idx === 'number'), + ); + + const restorable = []; + if (maxIndex === 3) return restorable; + for (let i = 4; i <= maxIndex; i++) { + if (!existingIndices.has(i)) { + restorable.push(i); + } + } + + return restorable; + } catch (err) { + return []; + } +} diff --git a/app/functions/accounts/handleEmoji.js b/app/functions/accounts/handleEmoji.js new file mode 100644 index 00000000..764a5fc9 --- /dev/null +++ b/app/functions/accounts/handleEmoji.js @@ -0,0 +1,493 @@ +export const SUGGESTED_EMOJIS = [ + '🔥', + '🔐', + '🔮', + '🖼️', + '💯', + '🔌', + '⚒️', + '🔗', + '🚀', + '🌙', + '💩', + '👻', + '👽', + '👾', + '🤖', + '😺', + '🥶', + '😶', + '😏', + '🤡', + '💎', + '🙌', + '🗣️', + '💪', + '💸', + '💵', + '🧠', + '📱', + '⚫', + '⚡', + '💰', + '🏦', + '📈', + '📉', + '💳', +]; + +export const EMOJI_CATEGORIES = [ + { + title: 'Suggested', + data: SUGGESTED_EMOJIS.map(emoji => ({ + emoji, + name: 'suggested', + shortName: 'suggested', + })), + }, + { + title: 'Smileys & Emotion', + data: [ + // 49 emojis (7 rows) + { emoji: '😀', name: 'grinning face', shortName: 'grinning' }, + { emoji: '😃', name: 'grinning face with big eyes', shortName: 'smiley' }, + { + emoji: '😄', + name: 'grinning face with smiling eyes', + shortName: 'smile', + }, + { + emoji: '😁', + name: 'beaming face with smiling eyes', + shortName: 'grin', + }, + { emoji: '😆', name: 'grinning squinting face', shortName: 'laughing' }, + { + emoji: '😅', + name: 'grinning face with sweat', + shortName: 'sweat_smile', + }, + { emoji: '🤣', name: 'rolling on the floor laughing', shortName: 'rofl' }, + { emoji: '😂', name: 'face with tears of joy', shortName: 'joy' }, + { + emoji: '🙂', + name: 'slightly smiling face', + shortName: 'slightly_smiling_face', + }, + { + emoji: '😊', + name: 'smiling face with smiling eyes', + shortName: 'blush', + }, + { emoji: '😇', name: 'smiling face with halo', shortName: 'innocent' }, + { + emoji: '🥰', + name: 'smiling face with hearts', + shortName: 'smiling_face_with_three_hearts', + }, + { + emoji: '😍', + name: 'smiling face with heart-eyes', + shortName: 'heart_eyes', + }, + { emoji: '🤩', name: 'star-struck', shortName: 'star_struck' }, + { emoji: '😘', name: 'face blowing a kiss', shortName: 'kissing_heart' }, + { emoji: '😋', name: 'face savoring food', shortName: 'yum' }, + { emoji: '😛', name: 'face with tongue', shortName: 'stuck_out_tongue' }, + { + emoji: '😜', + name: 'winking face with tongue', + shortName: 'stuck_out_tongue_winking_eye', + }, + { emoji: '🤪', name: 'zany face', shortName: 'zany_face' }, + { + emoji: '😎', + name: 'smiling face with sunglasses', + shortName: 'sunglasses', + }, + { emoji: '🤓', name: 'nerd face', shortName: 'nerd_face' }, + { emoji: '🧐', name: 'face with monocle', shortName: 'monocle_face' }, + { emoji: '😏', name: 'smirking face', shortName: 'smirk' }, + { emoji: '🤡', name: 'clown face', shortName: 'clown_face' }, + { emoji: '🥳', name: 'partying face', shortName: 'partying_face' }, + { emoji: '😤', name: 'face with steam from nose', shortName: 'triumph' }, + { + emoji: '🤬', + name: 'face with symbols on mouth', + shortName: 'cursing_face', + }, + { emoji: '🥺', name: 'pleading face', shortName: 'pleading_face' }, + { emoji: '😢', name: 'crying face', shortName: 'cry' }, + { emoji: '😭', name: 'loudly crying face', shortName: 'sob' }, + { emoji: '😱', name: 'face screaming in fear', shortName: 'scream' }, + { + emoji: '😈', + name: 'smiling face with horns', + shortName: 'smiling_imp', + }, + { emoji: '👿', name: 'angry face with horns', shortName: 'imp' }, + { emoji: '💀', name: 'skull', shortName: 'skull' }, + { emoji: '👻', name: 'ghost', shortName: 'ghost' }, + { emoji: '👽', name: 'alien', shortName: 'alien' }, + { emoji: '👾', name: 'alien monster', shortName: 'space_invader' }, + { emoji: '🤖', name: 'robot', shortName: 'robot' }, + { emoji: '😺', name: 'grinning cat', shortName: 'smiley_cat' }, + { + emoji: '😸', + name: 'grinning cat with smiling eyes', + shortName: 'smile_cat', + }, + { emoji: '😹', name: 'cat with tears of joy', shortName: 'joy_cat' }, + { + emoji: '😻', + name: 'smiling cat with heart-eyes', + shortName: 'heart_eyes_cat', + }, + { emoji: '🙀', name: 'weary cat', shortName: 'scream_cat' }, + { emoji: '💩', name: 'pile of poo', shortName: 'poop' }, + { emoji: '🤠', name: 'cowboy hat face', shortName: 'cowboy_hat_face' }, + { emoji: '🥶', name: 'cold face', shortName: 'cold_face' }, + { emoji: '😶', name: 'face without mouth', shortName: 'no_mouth' }, + { emoji: '🤑', name: 'money-mouth face', shortName: 'money_mouth_face' }, + { emoji: '😴', name: 'sleeping face', shortName: 'sleeping' }, + ], + }, + { + title: 'People & Body', + data: [ + // 28 emojis (4 rows) + { emoji: '👋', name: 'waving hand', shortName: 'wave' }, + { + emoji: '🤚', + name: 'raised back of hand', + shortName: 'raised_back_of_hand', + }, + { emoji: '✋', name: 'raised hand', shortName: 'hand' }, + { emoji: '🖖', name: 'vulcan salute', shortName: 'vulcan_salute' }, + { emoji: '👌', name: 'ok hand', shortName: 'ok_hand' }, + { emoji: '🤌', name: 'pinched fingers', shortName: 'pinched_fingers' }, + { emoji: '✌️', name: 'victory hand', shortName: 'v' }, + { emoji: '🤞', name: 'crossed fingers', shortName: 'crossed_fingers' }, + { emoji: '🤟', name: 'love-you gesture', shortName: 'love_you_gesture' }, + { emoji: '🤘', name: 'sign of the horns', shortName: 'metal' }, + { emoji: '🤙', name: 'call me hand', shortName: 'call_me_hand' }, + { + emoji: '👈', + name: 'backhand index pointing left', + shortName: 'point_left', + }, + { + emoji: '👉', + name: 'backhand index pointing right', + shortName: 'point_right', + }, + { + emoji: '👆', + name: 'backhand index pointing up', + shortName: 'point_up_2', + }, + { emoji: '👍', name: 'thumbs up', shortName: 'thumbsup' }, + { emoji: '👎', name: 'thumbs down', shortName: 'thumbsdown' }, + { emoji: '✊', name: 'raised fist', shortName: 'fist' }, + { emoji: '👊', name: 'oncoming fist', shortName: 'facepunch' }, + { emoji: '🤛', name: 'left-facing fist', shortName: 'fist_left' }, + { emoji: '🤜', name: 'right-facing fist', shortName: 'fist_right' }, + { emoji: '👏', name: 'clapping hands', shortName: 'clap' }, + { emoji: '🙌', name: 'raising hands', shortName: 'raised_hands' }, + { emoji: '👐', name: 'open hands', shortName: 'open_hands' }, + { emoji: '🙏', name: 'folded hands', shortName: 'pray' }, + { emoji: '💪', name: 'flexed biceps', shortName: 'muscle' }, + { emoji: '🦾', name: 'mechanical arm', shortName: 'mechanical_arm' }, + { emoji: '🗣️', name: 'speaking head', shortName: 'speaking_head' }, + { emoji: '👶', name: 'baby', shortName: 'baby' }, + ], + }, + { + title: 'Animals & Nature', + data: [ + // 35 emojis (5 rows) + { emoji: '🐶', name: 'dog face', shortName: 'dog' }, + { emoji: '🐱', name: 'cat face', shortName: 'cat' }, + { emoji: '🐭', name: 'mouse face', shortName: 'mouse' }, + { emoji: '🐹', name: 'hamster', shortName: 'hamster' }, + { emoji: '🐰', name: 'rabbit face', shortName: 'rabbit' }, + { emoji: '🦊', name: 'fox', shortName: 'fox_face' }, + { emoji: '🐻', name: 'bear', shortName: 'bear' }, + { emoji: '🐼', name: 'panda', shortName: 'panda_face' }, + { emoji: '🐨', name: 'koala', shortName: 'koala' }, + { emoji: '🐯', name: 'tiger face', shortName: 'tiger' }, + { emoji: '🦁', name: 'lion', shortName: 'lion' }, + { emoji: '🐮', name: 'cow face', shortName: 'cow' }, + { emoji: '🐷', name: 'pig face', shortName: 'pig' }, + { emoji: '🐸', name: 'frog', shortName: 'frog' }, + { emoji: '🐵', name: 'monkey face', shortName: 'monkey_face' }, + { emoji: '🙈', name: 'see-no-evil monkey', shortName: 'see_no_evil' }, + { emoji: '🙉', name: 'hear-no-evil monkey', shortName: 'hear_no_evil' }, + { emoji: '🙊', name: 'speak-no-evil monkey', shortName: 'speak_no_evil' }, + { emoji: '🐔', name: 'chicken', shortName: 'chicken' }, + { emoji: '🐧', name: 'penguin', shortName: 'penguin' }, + { emoji: '🦅', name: 'eagle', shortName: 'eagle' }, + { emoji: '🦉', name: 'owl', shortName: 'owl' }, + { emoji: '🐺', name: 'wolf', shortName: 'wolf' }, + { emoji: '🐝', name: 'honeybee', shortName: 'bee' }, + { emoji: '🦋', name: 'butterfly', shortName: 'butterfly' }, + { emoji: '🐙', name: 'octopus', shortName: 'octopus' }, + { emoji: '🐬', name: 'dolphin', shortName: 'dolphin' }, + { emoji: '🐳', name: 'spouting whale', shortName: 'whale' }, + { emoji: '🦈', name: 'shark', shortName: 'shark' }, + { emoji: '🌿', name: 'herb', shortName: 'herb' }, + { emoji: '🌻', name: 'sunflower', shortName: 'sunflower' }, + { emoji: '🌈', name: 'rainbow', shortName: 'rainbow' }, + { emoji: '🌲', name: 'evergreen tree', shortName: 'evergreen_tree' }, + { emoji: '🍀', name: 'four leaf clover', shortName: 'four_leaf_clover' }, + { emoji: '🌸', name: 'cherry blossom', shortName: 'cherry_blossom' }, + ], + }, + { + title: 'Food & Drink', + data: [ + // 28 emojis (4 rows) + { emoji: '🍎', name: 'red apple', shortName: 'apple' }, + { emoji: '🍊', name: 'tangerine', shortName: 'tangerine' }, + { emoji: '🍋', name: 'lemon', shortName: 'lemon' }, + { emoji: '🍌', name: 'banana', shortName: 'banana' }, + { emoji: '🍉', name: 'watermelon', shortName: 'watermelon' }, + { emoji: '🍇', name: 'grapes', shortName: 'grapes' }, + { emoji: '🍓', name: 'strawberry', shortName: 'strawberry' }, + { emoji: '🍑', name: 'peach', shortName: 'peach' }, + { emoji: '🥑', name: 'avocado', shortName: 'avocado' }, + { emoji: '🌶️', name: 'hot pepper', shortName: 'hot_pepper' }, + { emoji: '🍕', name: 'pizza', shortName: 'pizza' }, + { emoji: '🍔', name: 'hamburger', shortName: 'hamburger' }, + { emoji: '🍟', name: 'french fries', shortName: 'fries' }, + { emoji: '🌮', name: 'taco', shortName: 'taco' }, + { emoji: '🍜', name: 'steaming bowl', shortName: 'ramen' }, + { emoji: '🍣', name: 'sushi', shortName: 'sushi' }, + { emoji: '🍩', name: 'doughnut', shortName: 'doughnut' }, + { emoji: '🍪', name: 'cookie', shortName: 'cookie' }, + { emoji: '🎂', name: 'birthday cake', shortName: 'birthday' }, + { emoji: '🍿', name: 'popcorn', shortName: 'popcorn' }, + { emoji: '🍺', name: 'beer mug', shortName: 'beer' }, + { emoji: '🍻', name: 'clinking beer mugs', shortName: 'beers' }, + { emoji: '☕', name: 'hot beverage coffee', shortName: 'coffee' }, + { emoji: '🍷', name: 'wine glass', shortName: 'wine_glass' }, + { emoji: '🥂', name: 'clinking glasses', shortName: 'champagne_glass' }, + { emoji: '🧋', name: 'bubble tea', shortName: 'bubble_tea' }, + { emoji: '🥤', name: 'cup with straw', shortName: 'cup_with_straw' }, + { emoji: '🧃', name: 'beverage box juice', shortName: 'beverage_box' }, + ], + }, + { + title: 'Travel & Places', + data: [ + // 28 emojis (4 rows) + { emoji: '🚗', name: 'automobile car', shortName: 'car' }, + { emoji: '🚕', name: 'taxi', shortName: 'taxi' }, + { emoji: '🚌', name: 'bus', shortName: 'bus' }, + { emoji: '🚑', name: 'ambulance', shortName: 'ambulance' }, + { emoji: '🚒', name: 'fire engine', shortName: 'fire_engine' }, + { emoji: '🏎️', name: 'racing car', shortName: 'racing_car' }, + { emoji: '🚲', name: 'bicycle', shortName: 'bike' }, + { emoji: '🛵', name: 'motor scooter', shortName: 'motor_scooter' }, + { emoji: '🚀', name: 'rocket', shortName: 'rocket' }, + { emoji: '✈️', name: 'airplane', shortName: 'airplane' }, + { emoji: '🚁', name: 'helicopter', shortName: 'helicopter' }, + { emoji: '🏠', name: 'house', shortName: 'house' }, + { + emoji: '🏡', + name: 'house with garden', + shortName: 'house_with_garden', + }, + { emoji: '🏢', name: 'office building', shortName: 'office' }, + { emoji: '🏰', name: 'castle', shortName: 'castle' }, + { emoji: '🏝️', name: 'desert island', shortName: 'desert_island' }, + { emoji: '🌋', name: 'volcano', shortName: 'volcano' }, + { emoji: '⛰️', name: 'mountain', shortName: 'mountain' }, + { emoji: '🏔️', name: 'snow-capped mountain', shortName: 'mountain_snow' }, + { + emoji: '🌍', + name: 'globe earth africa europe', + shortName: 'earth_africa', + }, + { + emoji: '🌎', + name: 'globe earth americas', + shortName: 'earth_americas', + }, + { + emoji: '🌏', + name: 'globe earth asia australia', + shortName: 'earth_asia', + }, + { emoji: '🏖️', name: 'beach with umbrella', shortName: 'beach' }, + { emoji: '🗺️', name: 'world map', shortName: 'world_map' }, + { emoji: '🧳', name: 'luggage', shortName: 'luggage' }, + { + emoji: '🛳️', + name: 'passenger ship cruise', + shortName: 'passenger_ship', + }, + { emoji: '🚂', name: 'locomotive train', shortName: 'steam_locomotive' }, + { emoji: '🌙', name: 'crescent moon', shortName: 'crescent_moon' }, + ], + }, + { + title: 'Activities', + data: [ + // 21 emojis (3 rows) + { emoji: '⚽', name: 'soccer ball', shortName: 'soccer' }, + { emoji: '🏀', name: 'basketball', shortName: 'basketball' }, + { emoji: '🏈', name: 'american football', shortName: 'football' }, + { emoji: '⚾', name: 'baseball', shortName: 'baseball' }, + { emoji: '🎾', name: 'tennis', shortName: 'tennis' }, + { emoji: '🏐', name: 'volleyball', shortName: 'volleyball' }, + { emoji: '🎱', name: 'billiards pool', shortName: 'eight_ball' }, + { emoji: '🏓', name: 'ping pong', shortName: 'ping_pong' }, + { emoji: '🎯', name: 'bullseye direct hit', shortName: 'dart' }, + { emoji: '🎮', name: 'video game', shortName: 'video_game' }, + { emoji: '🕹️', name: 'joystick', shortName: 'joystick' }, + { emoji: '🎲', name: 'game die', shortName: 'game_die' }, + { + emoji: '🎭', + name: 'performing arts theater', + shortName: 'performing_arts', + }, + { emoji: '🎨', name: 'artist palette', shortName: 'art' }, + { emoji: '🎬', name: 'clapper board movie', shortName: 'clapper' }, + { emoji: '🎤', name: 'microphone', shortName: 'microphone' }, + { emoji: '🎧', name: 'headphone', shortName: 'headphones' }, + { emoji: '🎵', name: 'musical note', shortName: 'musical_note' }, + { emoji: '🎸', name: 'guitar', shortName: 'guitar' }, + { emoji: '🏆', name: 'trophy', shortName: 'trophy' }, + { emoji: '🏋️', name: 'weight lifting gym', shortName: 'weight_lifting' }, + ], + }, + { + title: 'Objects', + data: [ + // 35 emojis (5 rows) + { emoji: '💰', name: 'money bag', shortName: 'moneybag' }, + { emoji: '💵', name: 'dollar banknote', shortName: 'dollar' }, + { emoji: '💴', name: 'yen banknote', shortName: 'yen' }, + { emoji: '💶', name: 'euro banknote', shortName: 'euro' }, + { emoji: '💷', name: 'pound banknote', shortName: 'pound' }, + { emoji: '💎', name: 'gem stone diamond', shortName: 'gem' }, + { emoji: '💳', name: 'credit card', shortName: 'credit_card' }, + { + emoji: '💲', + name: 'heavy dollar sign', + shortName: 'heavy_dollar_sign', + }, + { emoji: '🪙', name: 'coin', shortName: 'coin' }, + { emoji: '🔑', name: 'key', shortName: 'key' }, + { emoji: '🗝️', name: 'old key', shortName: 'old_key' }, + { emoji: '🔒', name: 'locked', shortName: 'lock' }, + { emoji: '🔓', name: 'unlocked', shortName: 'unlock' }, + { + emoji: '🔐', + name: 'locked with key', + shortName: 'closed_lock_with_key', + }, + { emoji: '🛡️', name: 'shield', shortName: 'shield' }, + { emoji: '📱', name: 'mobile phone', shortName: 'iphone' }, + { emoji: '💻', name: 'laptop computer', shortName: 'computer' }, + { emoji: '🖥️', name: 'desktop computer', shortName: 'desktop_computer' }, + { emoji: '⌚', name: 'watch', shortName: 'watch' }, + { emoji: '📷', name: 'camera', shortName: 'camera' }, + { emoji: '💡', name: 'light bulb idea', shortName: 'bulb' }, + { emoji: '🔧', name: 'wrench', shortName: 'wrench' }, + { emoji: '🔨', name: 'hammer', shortName: 'hammer' }, + { emoji: '⚙️', name: 'gear settings', shortName: 'gear' }, + { emoji: '📦', name: 'package', shortName: 'package' }, + { emoji: '✉️', name: 'envelope mail', shortName: 'envelope' }, + { emoji: '📚', name: 'books', shortName: 'books' }, + { emoji: '✏️', name: 'pencil', shortName: 'pencil' }, + { emoji: '🎓', name: 'graduation cap', shortName: 'mortar_board' }, + { emoji: '⏰', name: 'alarm clock', shortName: 'alarm_clock' }, + { emoji: '🎁', name: 'wrapped gift present', shortName: 'gift' }, + { emoji: '🛍️', name: 'shopping bags', shortName: 'shopping' }, + { emoji: '📺', name: 'television', shortName: 'tv' }, + { emoji: '🛒', name: 'shopping cart', shortName: 'shopping_cart' }, + { emoji: '💊', name: 'pill medicine', shortName: 'pill' }, + ], + }, + { + title: 'Symbols', + data: [ + // 35 emojis (5 rows) + { emoji: '❤️', name: 'red heart love', shortName: 'heart' }, + { emoji: '🧡', name: 'orange heart', shortName: 'orange_heart' }, + { emoji: '💛', name: 'yellow heart', shortName: 'yellow_heart' }, + { emoji: '💚', name: 'green heart', shortName: 'green_heart' }, + { emoji: '💙', name: 'blue heart', shortName: 'blue_heart' }, + { emoji: '💜', name: 'purple heart', shortName: 'purple_heart' }, + { emoji: '🖤', name: 'black heart', shortName: 'black_heart' }, + { emoji: '🤍', name: 'white heart', shortName: 'white_heart' }, + { emoji: '💔', name: 'broken heart', shortName: 'broken_heart' }, + { + emoji: '❣️', + name: 'heart exclamation', + shortName: 'heavy_heart_exclamation', + }, + { emoji: '♻️', name: 'recycling symbol', shortName: 'recycle' }, + { emoji: '⚠️', name: 'warning', shortName: 'warning' }, + { emoji: '🚫', name: 'prohibited', shortName: 'no_entry_sign' }, + { emoji: '❌', name: 'cross mark', shortName: 'x' }, + { emoji: '✅', name: 'check mark button', shortName: 'white_check_mark' }, + { emoji: '❓', name: 'question mark', shortName: 'question' }, + { emoji: '❗', name: 'exclamation mark', shortName: 'exclamation' }, + { emoji: '➕', name: 'plus', shortName: 'heavy_plus_sign' }, + { emoji: '➖', name: 'minus', shortName: 'heavy_minus_sign' }, + { emoji: '✖️', name: 'multiply', shortName: 'heavy_multiplication_x' }, + { emoji: '♾️', name: 'infinity', shortName: 'infinity' }, + { emoji: '💯', name: 'hundred points', shortName: '100' }, + { emoji: '🔴', name: 'red circle', shortName: 'red_circle' }, + { emoji: '🟠', name: 'orange circle', shortName: 'orange_circle' }, + { emoji: '🟡', name: 'yellow circle', shortName: 'yellow_circle' }, + { emoji: '🟢', name: 'green circle', shortName: 'green_circle' }, + { emoji: '🔵', name: 'blue circle', shortName: 'blue_circle' }, + { emoji: '🟣', name: 'purple circle', shortName: 'purple_circle' }, + { emoji: '⚫', name: 'black circle', shortName: 'black_circle' }, + { emoji: '⚪', name: 'white circle', shortName: 'white_circle' }, + { emoji: '⭐', name: 'star', shortName: 'star' }, + { emoji: '🌟', name: 'glowing star', shortName: 'star2' }, + { emoji: '✨', name: 'sparkles', shortName: 'sparkles' }, + { emoji: '⚡', name: 'high voltage lightning', shortName: 'zap' }, + { emoji: '🔥', name: 'fire', shortName: 'fire' }, + ], + }, + { + title: 'Flags', + data: [ + // 28 emojis (4 rows) + { emoji: '🏳️', name: 'white flag', shortName: 'white_flag' }, + { emoji: '🏴', name: 'black flag', shortName: 'black_flag' }, + { emoji: '🏁', name: 'chequered flag', shortName: 'checkered_flag' }, + { emoji: '🏴‍☠️', name: 'pirate flag', shortName: 'pirate_flag' }, + { emoji: '🇺🇸', name: 'flag united states', shortName: 'us' }, + { emoji: '🇬🇧', name: 'flag united kingdom', shortName: 'gb' }, + { emoji: '🇨🇦', name: 'flag canada', shortName: 'canada' }, + { emoji: '🇦🇺', name: 'flag australia', shortName: 'australia' }, + { emoji: '🇩🇪', name: 'flag germany', shortName: 'de' }, + { emoji: '🇫🇷', name: 'flag france', shortName: 'fr' }, + { emoji: '🇪🇸', name: 'flag spain', shortName: 'es' }, + { emoji: '🇮🇹', name: 'flag italy', shortName: 'it' }, + { emoji: '🇧🇷', name: 'flag brazil', shortName: 'brazil' }, + { emoji: '🇯🇵', name: 'flag japan', shortName: 'jp' }, + { emoji: '🇰🇷', name: 'flag south korea', shortName: 'kr' }, + { emoji: '🇨🇳', name: 'flag china', shortName: 'cn' }, + { emoji: '🇮🇳', name: 'flag india', shortName: 'india' }, + { emoji: '🇲🇽', name: 'flag mexico', shortName: 'mexico' }, + { emoji: '🇦🇷', name: 'flag argentina', shortName: 'argentina' }, + { emoji: '🇨🇴', name: 'flag colombia', shortName: 'colombia' }, + { emoji: '🇳🇬', name: 'flag nigeria', shortName: 'nigeria' }, + { emoji: '🇸🇪', name: 'flag sweden', shortName: 'sweden' }, + { emoji: '🇳🇱', name: 'flag netherlands', shortName: 'netherlands' }, + { emoji: '🇨🇭', name: 'flag switzerland', shortName: 'switzerland' }, + { emoji: '🇵🇹', name: 'flag portugal', shortName: 'portugal' }, + { emoji: '🇵🇱', name: 'flag poland', shortName: 'poland' }, + { emoji: '🇹🇷', name: 'flag turkey', shortName: 'turkey' }, + { emoji: '🇸🇻', name: 'flag el salvador', shortName: 'el_salvador' }, + ], + }, +]; diff --git a/app/functions/initializeUserSettings.js b/app/functions/initializeUserSettings.js index 41c8ad89..27d6e140 100644 --- a/app/functions/initializeUserSettings.js +++ b/app/functions/initializeUserSettings.js @@ -91,6 +91,7 @@ export default async function initializeUserSettingsFromHistory({ defaultSpendToken, thousandsSeperator, enabledLiquidAutoSwap, + pinnedAccounts, } = localStoredData; if (blitzStoredData === null) throw Error('Failed to retrive'); @@ -129,6 +130,12 @@ export default async function initializeUserSettingsFromHistory({ const currentDerivedGiftIndex = blitzStoredData.currentDerivedGiftIndex || 1; + const nextAccountDerivationIndex = + blitzStoredData.nextAccountDerivationIndex || 3; + + const currentDerivedPoolIndex = + blitzStoredData.currentDerivedPoolIndex || 1; + let pushNotifications = blitzStoredData.pushNotifications || { isEnabled: false, pushNotifications: { @@ -388,6 +395,9 @@ export default async function initializeUserSettingsFromHistory({ tempObject['currentDerivedGiftIndex'] = currentDerivedGiftIndex; tempObject['thousandsSeperator'] = thousandsSeperator; tempObject['enabledLiquidAutoSwap'] = enabledLiquidAutoSwap; + tempObject['pinnedAccounts'] = pinnedAccounts; + tempObject['nextAccountDerivationIndex'] = nextAccountDerivationIndex; + tempObject['currentDerivedPoolIndex'] = currentDerivedPoolIndex; // store in contacts context tempObject['contacts'] = contacts; diff --git a/app/functions/initializeUserSettingsHelpers/index.js b/app/functions/initializeUserSettingsHelpers/index.js index 46f650ce..4a2efe29 100644 --- a/app/functions/initializeUserSettingsHelpers/index.js +++ b/app/functions/initializeUserSettingsHelpers/index.js @@ -25,6 +25,7 @@ const keys = [ 'defaultSpendToken', 'thousandsSeperator', 'enabledLiquidAutoSwap', + 'pinnedAccounts', ]; const defaultValues = { @@ -60,6 +61,7 @@ const defaultValues = { defaultSpendToken: 'Bitcoin', thousandsSeperator: 'space', enabledLiquidAutoSwap: true, + pinnedAccounts: [], }; export const fetchLocalStorageItems = async () => { @@ -104,6 +106,7 @@ export const fetchLocalStorageItems = async () => { thousandsSeperator: parsedResults[19] ?? defaultValues.thousandsSeperator, enabledLiquidAutoSwap: parsedResults[20] ?? defaultValues.enabledLiquidAutoSwap, + pinnedAccounts: parsedResults[21] ?? defaultValues.pinnedAccounts, }; }; diff --git a/app/hooks/useAccountSwitcher.js b/app/hooks/useAccountSwitcher.js new file mode 100644 index 00000000..091260b1 --- /dev/null +++ b/app/hooks/useAccountSwitcher.js @@ -0,0 +1,88 @@ +import { useCallback, useMemo, useState } from 'react'; +import { useNavigation } from '@react-navigation/native'; +import { useActiveCustodyAccount } from '../../context-store/activeAccount'; +import { useSparkWallet } from '../../context-store/sparkContext'; +import { initWallet } from '../functions/initiateWalletConnection'; + +export default function useAccountSwitcher() { + const navigate = useNavigation(); + const { setSparkInformation } = useSparkWallet(); + const { + currentWalletMnemoinc, + selectedAltAccount, + getAccountMnemonic, + updateAccountCacheOnly, + toggleIsUsingNostr, + isUsingNostr, + custodyAccountsList, + activeAccount, + } = useActiveCustodyAccount(); + + const [isSwitchingAccount, setIsSwitchingAccount] = useState({ + accountBeingLoaded: '', + isLoading: false, + }); + + const handleAccountPress = useCallback( + async account => { + try { + const accountMnemonic = await getAccountMnemonic(account); + if (currentWalletMnemoinc === accountMnemonic) return; + + setIsSwitchingAccount({ + accountBeingLoaded: account.uuid || account.name, + isLoading: true, + }); + + await new Promise(resolve => setTimeout(resolve, 250)); + + const initResponse = await initWallet({ + setSparkInformation, + mnemonic: accountMnemonic, + }); + + if (!initResponse.didWork) { + navigate.navigate('ErrorScreen', { + errorMessage: initResponse.error, + }); + return; + } + + const isMainWallet = account.name === 'Main Wallet'; + const isNWC = account.name === 'NWC'; + + if (isMainWallet || isNWC) { + if (selectedAltAccount[0]) { + await updateAccountCacheOnly({ + ...selectedAltAccount[0], + isActive: false, + }); + } + toggleIsUsingNostr(isNWC); + } else { + await updateAccountCacheOnly({ ...account, isActive: true }); + toggleIsUsingNostr(false); + } + } catch (error) { + navigate.navigate('ErrorScreen', { + errorMessage: error.message || 'An error occurred', + }); + } finally { + setIsSwitchingAccount({ + accountBeingLoaded: '', + isLoading: false, + }); + } + }, + [currentWalletMnemoinc, selectedAltAccount], + ); + + return { + accounts: custodyAccountsList, + activeAccount, + isSwitchingAccount, + handleAccountPress, + isUsingNostr, + selectedAltAccount, + }; +} diff --git a/app/hooks/useCustodyAccountsList.js b/app/hooks/useCustodyAccountsList.js index eeb7fd96..11c7a74f 100644 --- a/app/hooks/useCustodyAccountsList.js +++ b/app/hooks/useCustodyAccountsList.js @@ -1,24 +1,42 @@ -import {useMemo} from 'react'; -import {useActiveCustodyAccount} from '../../context-store/activeAccount'; -import {useGlobalContextProvider} from '../../context-store/context'; -import {useKeysContext} from '../../context-store/keys'; +import { useMemo } from 'react'; +import { useActiveCustodyAccount } from '../../context-store/activeAccount'; +import { useGlobalContextProvider } from '../../context-store/context'; +import { useKeysContext } from '../../context-store/keys'; export default function useCustodyAccountList() { - const {accountMnemoinc} = useKeysContext(); - const {custodyAccounts, nostrSeed} = useActiveCustodyAccount(); - const {masterInfoObject} = useGlobalContextProvider(); + const { accountMnemoinc } = useKeysContext(); + const { custodyAccounts, nostrSeed } = useActiveCustodyAccount(); + const { masterInfoObject } = useGlobalContextProvider(); const enabledNWC = masterInfoObject.didViewNWCMessage; const accounts = useMemo(() => { return enabledNWC ? [ - {name: 'Main Wallet', mnemoinc: accountMnemoinc}, - {name: 'NWC', mnemoinc: nostrSeed}, + { + name: 'Main Wallet', + mnemoinc: accountMnemoinc, + accountType: 'main', + uuid: 'MW09xd09d8f0a9sf2n332', + }, + { + name: 'NWC', + mnemoinc: nostrSeed, + accountType: 'nwc', + uuid: 'NWC038rsd0f8234ajsf', + }, ...custodyAccounts, ] - : [{name: 'Main Wallet', mnemoinc: accountMnemoinc}, ...custodyAccounts]; - }, [custodyAccounts, enabledNWC]); + : [ + { + name: 'Main Wallet', + mnemoinc: accountMnemoinc, + accountType: 'main', + uuid: 'MW09xd09d8f0a9sf2n332', + }, + ...custodyAccounts, + ]; + }, [accountMnemoinc, custodyAccounts, enabledNWC, nostrSeed]); return accounts; } diff --git a/app/screens/inAccount/index.js b/app/screens/inAccount/index.js index f64b7595..1fbc99b7 100644 --- a/app/screens/inAccount/index.js +++ b/app/screens/inAccount/index.js @@ -17,7 +17,7 @@ import SettingsIndex from './settingsIndex'; import TechnicalTransactionDetails from './technicalTransactionDetails'; import ViewAllTxPage from './viewAllTxPage'; import SwapsPage from './swapPage'; - +import SettingsHub from './settingsHub'; export { ExpandedTx, AdminLogin, @@ -37,4 +37,5 @@ export { AppStorePageIndex, BuyBitcoinHome, SwapsPage, + SettingsHub, }; diff --git a/app/screens/inAccount/settingsContent.js b/app/screens/inAccount/settingsContent.js index 8f3b66c4..6e1a7fb4 100644 --- a/app/screens/inAccount/settingsContent.js +++ b/app/screens/inAccount/settingsContent.js @@ -8,6 +8,7 @@ import { PosSettingsPage, ResetPage, SeedPhrasePage, + SeedPhraseWarning, CrashReportingSettingsPage, CreateCustodyAccounts, SparkInfo, @@ -67,18 +68,6 @@ export default function SettingsContentIndex(props) { { - if (selectedPage?.toLowerCase() === 'backup wallet') { - navigate.navigate('InformationPopup', { - textContent: t('settings.index.seedPopup'), - buttonText: t('constants.iunderstand'), - }); - } - }} label={t( `screens.inAccount.settingsContent.${selectedPage.toLowerCase()}`, )} @@ -128,6 +117,13 @@ export default function SettingsContentIndex(props) { )} {selectedPage?.toLowerCase() === 'backup wallet' && ( + + )} + {selectedPage?.toLowerCase() === 'show seed phrase' && ( )} {selectedPage?.toLowerCase() === 'spark info' && ( diff --git a/app/screens/inAccount/settingsHub/components/AccountsPreview.js b/app/screens/inAccount/settingsHub/components/AccountsPreview.js new file mode 100644 index 00000000..5cfe31f3 --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/AccountsPreview.js @@ -0,0 +1,173 @@ +import { Pressable, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import AccountCard from '../../../../components/admin/homeComponents/accounts/accountCard'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { SIZES } from '../../../../constants'; +import { useTranslation } from 'react-i18next'; +import { useActiveCustodyAccount } from '../../../../../context-store/activeAccount'; + +export default function AccountsPreview({ + accounts, + pinnedAccountUUIDs, + isUsingNostr, + selectedAltAccount, + isSwitchingAccount, + onAccountPress, + onAccountEdit, + onViewAll, +}) { + const { backgroundOffset } = GetThemeColors(); + const { t } = useTranslation(); + const { custodyAccountsList, activeAccount } = useActiveCustodyAccount(); + const displayAccounts = getDisplayAccounts( + custodyAccountsList, + pinnedAccountUUIDs, + isUsingNostr, + selectedAltAccount[0], + ); + + const hasMoreAccounts = custodyAccountsList?.length > displayAccounts?.length; + + return ( + [ + styles.card, + { backgroundColor: backgroundOffset }, + pressed && styles.pressed, + ]} + > + {/* Header becomes just visual */} + + + + + + + {displayAccounts.map((account, index) => ( + onAccountPress(account)} + onEdit={() => onAccountEdit(account)} + isLoading={ + isSwitchingAccount.accountBeingLoaded === + (account.uuid || account.name) && isSwitchingAccount.isLoading + } + fromSettings + /> + ))} + + + {hasMoreAccounts && ( + + )} + + ); +} + +function getDisplayAccounts( + accounts, + pinnedAccountUUIDs, + isUsingNostr, + activeAltAccount, +) { + const MAIN_UUID = 'MW09xd09d8f0a9sf2n332'; + + const mainIndex = accounts.findIndex(a => a.uuid === MAIN_UUID); + + const orderedAccounts = + mainIndex > 0 + ? [ + accounts[mainIndex], + ...accounts.slice(0, mainIndex), + ...accounts.slice(mainIndex + 1), + ] + : accounts; + + const mainAccount = orderedAccounts[0]; + + if (pinnedAccountUUIDs?.length) { + const pinned = pinnedAccountUUIDs + .map(uuid => orderedAccounts.find(a => (a.uuid || a.name) === uuid)) + .filter(Boolean) + .filter(a => a.uuid !== MAIN_UUID); + + if (pinned.length) { + return [mainAccount, ...pinned.slice(0, 2)]; + } + } + + const activeIndex = orderedAccounts.findIndex(account => { + const isMainWallet = account.name === 'Main Wallet'; + const isNWC = account.name === 'NWC'; + + return isNWC + ? isUsingNostr + : isMainWallet + ? !activeAltAccount && !isUsingNostr + : activeAltAccount?.uuid === account.uuid; + }); + + const active = + activeIndex >= 0 ? orderedAccounts[activeIndex] : orderedAccounts[0]; + + const next = orderedAccounts.find((_, i) => i !== activeIndex); + + const result = [active, next].filter(Boolean); + + if (result[0]?.uuid !== MAIN_UUID) { + return [mainAccount, ...result.filter(a => a.uuid !== MAIN_UUID)].slice( + 0, + 2, + ); + } + + return result.slice(0, 2); +} + +const styles = StyleSheet.create({ + card: { + width: '100%', + borderRadius: 16, + padding: 12, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 4, + paddingBottom: 4, + }, + headerTitle: { + fontSize: SIZES.smedium, + fontWeight: '500', + includeFontPadding: false, + }, + viewAll: { + fontSize: SIZES.small, + opacity: 0.6, + includeFontPadding: false, + }, + moreText: { + fontSize: SIZES.small, + opacity: 0.6, + marginTop: 4, + includeFontPadding: false, + }, + pressed: { + opacity: 0.7, + }, +}); diff --git a/app/screens/inAccount/settingsHub/components/PointOfSaleBanner.js b/app/screens/inAccount/settingsHub/components/PointOfSaleBanner.js new file mode 100644 index 00000000..7500d7ac --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/PointOfSaleBanner.js @@ -0,0 +1,53 @@ +import { StyleSheet, TouchableOpacity } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; +import { useGlobalThemeContext } from '../../../../../context-store/theme'; +import { COLORS, SIZES } from '../../../../constants'; +import { useTranslation } from 'react-i18next'; + +export default function PointOfSaleBanner({ onPress }) { + const { theme, darkModeType } = useGlobalThemeContext(); + const { t } = useTranslation(); + + const accentColor = theme && darkModeType ? COLORS.white : COLORS.primary; + + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + banner: { + width: '100%', + flexDirection: 'row', + borderWidth: 2, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + }, + text: { + flexShrink: 1, + fontSize: SIZES.xLarge, + marginLeft: 10, + includeFontPadding: false, + }, +}); diff --git a/app/screens/inAccount/settingsHub/components/PoolsPreview.js b/app/screens/inAccount/settingsHub/components/PoolsPreview.js new file mode 100644 index 00000000..eccc1e63 --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/PoolsPreview.js @@ -0,0 +1,123 @@ +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import CircularProgress from '../../../../components/admin/homeComponents/pools/circularProgress'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { SIZES } from '../../../../constants'; +import { useTranslation } from 'react-i18next'; + +export default function PoolsPreview({ + activePoolsArray, + poolsArray, + onViewAll, +}) { + const { backgroundOffset } = GetThemeColors(); + const { t } = useTranslation(); + + const displayedPools = activePoolsArray.slice(0, 2); + const hasMorePools = activePoolsArray.length > 2; + const remainingPoolsCount = activePoolsArray.length - 2; + + return ( + + + + {!!poolsArray.length && ( + + )} + + {activePoolsArray.length > 0 ? ( + <> + {displayedPools.map(pool => ( + + + + + ))} + {hasMorePools && ( + + )} + + ) : ( + + )} + + ); +} + +const styles = StyleSheet.create({ + card: { + width: '100%', + borderRadius: 16, + padding: 16, + }, + header: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 8, + }, + headerTitle: { + fontSize: SIZES.smedium, + fontWeight: '500', + includeFontPadding: false, + }, + viewAll: { + fontSize: SIZES.small, + opacity: 0.6, + includeFontPadding: false, + }, + poolRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + paddingVertical: 4, + }, + poolTitle: { + flex: 1, + fontSize: SIZES.medium, + includeFontPadding: false, + }, + moreText: { + fontSize: SIZES.small, + opacity: 0.6, + marginTop: 4, + includeFontPadding: false, + }, + emptyText: { + fontSize: SIZES.smedium, + opacity: 0.5, + includeFontPadding: false, + }, +}); diff --git a/app/screens/inAccount/settingsHub/components/ProfileCard.js b/app/screens/inAccount/settingsHub/components/ProfileCard.js new file mode 100644 index 00000000..127733d7 --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/ProfileCard.js @@ -0,0 +1,197 @@ +import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import ContactProfileImage from '../../../../components/admin/homeComponents/contacts/internalComponents/profileImage'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { useGlobalThemeContext } from '../../../../../context-store/theme'; +import { CENTER, COLORS, SIZES } from '../../../../constants'; +import { useTranslation } from 'react-i18next'; + +export default function ProfileCard({ + profileImage, + name, + uniqueName, + onEditPress, + onShowQRPress, + onCopyUsername, +}) { + const { backgroundOffset, backgroundColor } = GetThemeColors(); + const { theme, darkModeType } = useGlobalThemeContext(); + const { t } = useTranslation(); + + return ( + + + + + + + + + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + card: { + width: '100%', + borderRadius: 16, + padding: 16, + }, + rowContainer: { + flexDirection: 'row', + }, + topRow: { + width: '100%', + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + }, + avatarContainer: { + width: 56, + height: 56, + borderRadius: 28, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + infoContainer: { + flex: 1, + marginLeft: 12, + }, + name: { + fontSize: SIZES.medium, + fontWeight: '500', + includeFontPadding: false, + }, + username: { + fontSize: SIZES.smedium, + opacity: 0.6, + includeFontPadding: false, + }, + actionsRow: { + flexDirection: 'row', + marginTop: 12, + gap: 8, + }, + actionButton: { + width: 40, + height: 32, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + shareButton: { + width: 35, + height: 35, + borderRadius: 8, + alignItems: 'center', + justifyContent: 'center', + }, + buttonContainer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + gap: 10, + // flexWrap: 'wrap', + }, + button: { + width: '100%', + minHeight: 50, + flexShrink: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 5, + paddingHorizontal: 8, + borderRadius: 12, + marginBottom: 15, + }, + buttonImage: { width: 20, height: 20, marginRight: 15 }, + profileImage: { + width: 125, + height: 125, + borderRadius: 125, + backgroundColor: 'red', + ...CENTER, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 15, + marginTop: 20, + overflow: 'hidden', + }, + selectFromPhotos: { + width: 30, + height: 30, + borderRadius: 20, + backgroundColor: COLORS.darkModeText, + alignItems: 'center', + justifyContent: 'center', + position: 'absolute', + right: 8, + bottom: 8, + zIndex: 2, + }, + profileContainer: { + width: '100%', + flexDirection: 'column', + alignItems: 'center', + // paddingBottom: 30, + // borderBottomWidth: 2, + }, + profileUniqueName: { marginBottom: 20 }, +}); diff --git a/app/screens/inAccount/settingsHub/components/SectionCard.js b/app/screens/inAccount/settingsHub/components/SectionCard.js new file mode 100644 index 00000000..0a1b4834 --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/SectionCard.js @@ -0,0 +1,36 @@ +import { StyleSheet, View } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { SIZES } from '../../../../constants'; + +export default function SectionCard({ title, children }) { + const { backgroundOffset } = GetThemeColors(); + + return ( + + {title ? : null} + + {children} + + + ); +} + +const styles = StyleSheet.create({ + wrapper: { + width: '100%', + }, + title: { + fontSize: SIZES.small, + textTransform: 'uppercase', + letterSpacing: 0.8, + opacity: 0.5, + marginBottom: 8, + marginLeft: 4, + includeFontPadding: false, + }, + card: { + borderRadius: 16, + overflow: 'hidden', + }, +}); diff --git a/app/screens/inAccount/settingsHub/components/SettingsRow.js b/app/screens/inAccount/settingsHub/components/SettingsRow.js new file mode 100644 index 00000000..c36d73de --- /dev/null +++ b/app/screens/inAccount/settingsHub/components/SettingsRow.js @@ -0,0 +1,78 @@ +import { StyleSheet, TouchableOpacity } from 'react-native'; +import { ThemeText } from '../../../../functions/CustomElements'; +import ThemeIcon from '../../../../functions/CustomElements/themeIcon'; +import ThemeImage from '../../../../functions/CustomElements/themeImage'; +import GetThemeColors from '../../../../hooks/themeColors'; +import { SIZES } from '../../../../constants'; + +export default function SettingsRow({ + iconName, + iconImage, + iconImageWhite, + label, + inlineValue, + onPress, + isLast, +}) { + const { backgroundColor } = GetThemeColors(); + + return ( + + {iconName ? ( + + ) : iconImage ? ( + + ) : null} + + {inlineValue ? ( + + ) : null} + + + ); +} + +const styles = StyleSheet.create({ + row: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 16, + minHeight: 48, + }, + iconImage: { + width: 20, + height: 20, + }, + label: { + flex: 1, + fontSize: SIZES.medium, + marginLeft: 12, + includeFontPadding: false, + }, + inlineValue: { + fontSize: SIZES.small, + opacity: 0.5, + marginRight: 8, + includeFontPadding: false, + }, +}); diff --git a/app/screens/inAccount/settingsHub/index.js b/app/screens/inAccount/settingsHub/index.js new file mode 100644 index 00000000..aa801e6a --- /dev/null +++ b/app/screens/inAccount/settingsHub/index.js @@ -0,0 +1,552 @@ +import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; +import { GlobalThemeView, ThemeText } from '../../../functions/CustomElements'; +import ThemeIcon from '../../../functions/CustomElements/themeIcon'; +import { useNavigation } from '@react-navigation/native'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { COLORS, ICONS, SIZES } from '../../../constants'; +import { INSET_WINDOW_WIDTH } from '../../../constants/theme'; +import { CENTER } from '../../../constants/styles'; +import { useGlobalThemeContext } from '../../../../context-store/theme'; +import { useGlobalContextProvider } from '../../../../context-store/context'; +import { useGlobalContacts } from '../../../../context-store/globalContacts'; +import { useImageCache } from '../../../../context-store/imageCache'; +import { useAppStatus } from '../../../../context-store/appStatus'; +import { usePools } from '../../../../context-store/poolContext'; +import { useToast } from '../../../../context-store/toastManager'; +import GetThemeColors from '../../../hooks/themeColors'; +import useAccountSwitcher from '../../../hooks/useAccountSwitcher'; +import { copyToClipboard } from '../../../functions'; +import { shareMessage } from '../../../functions/handleShare'; +import openWebBrowser from '../../../functions/openWebBrowser'; +import { supportedLanguagesList } from '../../../../locales/localeslist'; + +import ProfileCard from './components/ProfileCard'; +import AccountsPreview from './components/AccountsPreview'; +import PoolsPreview from './components/PoolsPreview'; +import SectionCard from './components/SectionCard'; +import SettingsRow from './components/SettingsRow'; +import PointOfSaleBanner from './components/PointOfSaleBanner'; +import { BlitzSocialOptions } from '../../../components/admin/homeComponents/settingsContent'; +import CustomSettingsTopBar from '../../../functions/CustomElements/settingsTopBar'; +import { useGlobalInsets } from '../../../../context-store/insetsProvider'; +import Animated, { + Extrapolation, + interpolate, + useAnimatedScrollHandler, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated'; + +const PREFERENCES_ROWS = [ + { + name: 'Display Currency', + displayName: 'screens.inAccount.settingsContent.display currency', + iconName: 'Coins', + hasInlineValue: 'fiatCurrency', + }, + { + name: 'Language', + displayName: 'screens.inAccount.settingsContent.language', + iconName: 'Languages', + hasInlineValue: 'language', + }, + { + name: 'Display Options', + displayName: 'screens.inAccount.settingsContent.display options', + iconName: 'Palette', + }, + { + name: 'Fast Pay', + displayName: 'screens.inAccount.settingsContent.fast pay', + iconName: 'ClockFading', + }, + { + name: 'Notifications', + displayName: 'screens.inAccount.settingsContent.notifications', + iconName: 'Bell', + }, +]; + +const SECURITY_ROWS = [ + { + name: 'Login Mode', + displayName: 'screens.inAccount.settingsContent.login mode', + iconName: 'ScanFace', + }, + { + name: 'Backup wallet', + displayName: 'screens.inAccount.settingsContent.backup wallet', + iconName: 'Lock', + }, +]; + +const TECHNICAL_ROWS = [ + { + name: 'Spark Info', + displayName: 'screens.inAccount.settingsContent.spark info', + iconName: 'VectorSquare', + }, + { + name: 'Nostr', + displayName: 'screens.inAccount.settingsContent.nostr', + iconName: 'Link', + }, + { + name: 'Blitz Fee Details', + displayName: 'screens.inAccount.settingsContent.blitz fee details', + iconImage: ICONS.receiptIcon, + iconImageWhite: ICONS.receiptWhite, + }, + { + name: 'Crash Reports', + displayName: 'screens.inAccount.settingsContent.crash reports', + iconName: 'ShieldCheck', + }, + { + name: 'ViewAllSwaps', + displayName: 'screens.inAccount.settingsContent.view all swaps', + iconName: 'SendToBack', + }, +]; + +const OTHER_ROWS = [ + { + name: 'About', + displayName: 'screens.inAccount.settingsContent.about', + iconName: 'Info', + }, + { + name: 'Blitz Stats', + displayName: 'screens.inAccount.settingsContent.blitz stats', + iconName: 'ChartArea', + }, +]; + +const DELETE_ROW = { + name: 'Delete Wallet', + displayName: 'screens.inAccount.settingsContent.delete wallet', + iconName: 'Trash2', + isDestructive: true, +}; + +const REQUIRES_INTERNET = [ + 'display currency', + 'fast pay', + 'point-of-sale', + 'edit contact profile', +]; +const SCROLL_THRESHOLD = 330; + +export default function SettingsHub(props) { + const navigate = useNavigation(); + const { t } = useTranslation(); + const { showToast } = useToast(); + const { theme, darkModeType } = useGlobalThemeContext(); + const { masterInfoObject } = useGlobalContextProvider(); + const { globalContactsInformation } = useGlobalContacts(); + const { cache } = useImageCache(); + const { isConnectedToTheInternet } = useAppStatus(); + const { backgroundOffset } = GetThemeColors(); + const { activePoolsArray, poolsArray } = usePools(); + const { bottomPadding } = useGlobalInsets(); + + const { + accounts, + activeAccount, + isSwitchingAccount, + handleAccountPress, + isUsingNostr, + selectedAltAccount, + } = useAccountSwitcher(); + + const isDoomsday = props?.route?.params?.isDoomsday; + const myProfileImage = cache[masterInfoObject?.uuid]; + const myContact = globalContactsInformation?.myProfile; + const pinnedAccountUUIDs = masterInfoObject.pinnedAccounts; + + const currentLanguage = supportedLanguagesList.find( + item => item.id === masterInfoObject.userSelectedLanguage, + )?.languageName; + + const scrollY = useSharedValue(0); + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: event => { + scrollY.value = event.contentOffset.y; + }, + }); + + const shareIconStyle = useAnimatedStyle(() => { + const opacity = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [1, 0], + Extrapolation.CLAMP, + ); + + const translateY = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [0, -10], + Extrapolation.CLAMP, + ); + + return { + opacity, + transform: [{ translateY }], + }; + }); + + const profileTextStyle = useAnimatedStyle(() => { + const opacity = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [1, 0], + Extrapolation.CLAMP, + ); + + const translateY = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [0, -10], + Extrapolation.CLAMP, + ); + + return { + opacity, + transform: [{ translateY }], + position: 'absolute', + }; + }); + + const settingsTextStyle = useAnimatedStyle(() => { + const opacity = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [0, 1], + Extrapolation.CLAMP, + ); + + const translateY = interpolate( + scrollY.value, + [SCROLL_THRESHOLD - 50, SCROLL_THRESHOLD], + [10, 0], + Extrapolation.CLAMP, + ); + + return { + opacity, + transform: [{ translateY }], + position: 'absolute', + }; + }); + + const handleSettingsRowPress = useCallback( + row => { + if ( + !isConnectedToTheInternet && + REQUIRES_INTERNET.includes(row.name.toLowerCase()) + ) { + navigate.navigate('ErrorScreen', { + errorMessage: t('errormessages.nointernet'), + }); + return; + } + navigate.navigate('SettingsContentHome', { + for: row.name, + isDoomsday, + }); + }, + [isConnectedToTheInternet, isDoomsday], + ); + + const handleEditProfile = useCallback(() => { + if (!isConnectedToTheInternet) { + navigate.navigate('ErrorScreen', { + errorMessage: t('errormessages.nointernet'), + }); + return; + } + navigate.navigate('SettingsContentHome', { + for: 'edit contact profile', + isDoomsday, + }); + }, [isConnectedToTheInternet, isDoomsday]); + + const handleShowQR = useCallback(() => { + navigate.navigate('ShowProfileQr'); + }, []); + + const handleCopyUsername = useCallback(() => { + copyToClipboard(myContact?.uniqueName, showToast); + }, [myContact?.uniqueName]); + + const handleAccountEdit = useCallback(account => { + navigate.navigate('EditAccountPage', { + account, + from: 'SettingsHome', + }); + }, []); + + const handleViewAllAccounts = useCallback(() => { + navigate.navigate('SettingsContentHome', { for: 'Accounts' }); + }, []); + + const handleViewAllPools = useCallback(() => { + navigate.navigate('SettingsContentHome', { for: 'pools' }); + }, []); + + const handlePOS = useCallback(() => { + if (!isConnectedToTheInternet) { + navigate.navigate('ErrorScreen', { + errorMessage: t('errormessages.nointernet'), + }); + return; + } + navigate.navigate('SettingsContentHome', { for: 'Point-of-sale' }); + }, [isConnectedToTheInternet]); + + const handleBlitzRestore = useCallback(() => { + openWebBrowser({ + navigate, + link: 'https://recover.blitzwalletapp.com/', + }); + }, []); + + const getInlineValue = useCallback( + row => { + if (row.hasInlineValue === 'fiatCurrency') { + return masterInfoObject.fiatCurrency?.toUpperCase(); + } + if (row.hasInlineValue === 'language') { + return currentLanguage; + } + return undefined; + }, + [masterInfoObject.fiatCurrency, currentLanguage], + ); + + const renderSection = useCallback( + (rows, title) => { + return ( + + {rows.map((row, index) => ( + handleSettingsRowPress(row)} + isLast={index === rows.length - 1} + isDestructive={row.isDestructive} + /> + ))} + + ); + }, + [t, getInlineValue, handleSettingsRowPress], + ); + + if (isDoomsday) { + return ( + + + + {renderSection( + [SECURITY_ROWS.find(r => r.name === 'Backup wallet')], + '', + )} + {renderSection([DELETE_ROW], '')} + + + + + + ); + } + + return ( + + + + + + + + + + + + + + + + {!isDoomsday && ( + + { + shareMessage({ + message: `${t( + 'share.contact', + )}\nhttps://blitzwalletapp.com/u/${myContact?.uniqueName}`, + }); + }} + > + + + + )} + + + + + + + + + {renderSection( + PREFERENCES_ROWS, + t('screens.inAccount.settingsContent.preferences'), + )} + + {renderSection( + SECURITY_ROWS, + t('screens.inAccount.settingsContent.security'), + )} + + {renderSection( + TECHNICAL_ROWS, + t('screens.inAccount.settingsContent.technical settings'), + )} + + {renderSection(OTHER_ROWS)} + + {renderSection([DELETE_ROW])} + + + + + + + ); +} + +const styles = StyleSheet.create({ + globalContainer: { + paddingBottom: 0, + }, + topBar: { + flexDirection: 'row', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + marginBottom: 10, + minHeight: 30, + }, + topBarTitle: { + fontSize: SIZES.large, + includeFontPadding: false, + }, + topBarSpacer: { + width: 24, + }, + scrollContent: { + width: INSET_WINDOW_WIDTH, + ...CENTER, + paddingTop: 8, + gap: 25, + }, + restoreBanner: { + width: '100%', + borderWidth: 2, + paddingHorizontal: 24, + paddingVertical: 12, + borderRadius: 16, + marginTop: 16, + alignItems: 'center', + }, + customTopbar: { + flexDirection: 'row', + width: '100%', + alignItems: 'center', + justifyContent: 'center', + marginBottom: 10, + minHeight: 30, + }, + goBackTopbar: { marginRight: 'auto' }, + topBarLabel: { + fontSize: SIZES.large, + flexShrink: 1, + includeFontPadding: false, + }, + headerTextContainer: { + width: '100%', + paddingHorizontal: 35, + position: 'absolute', + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/app/screens/toast.js b/app/screens/toast.js index c1396856..d2d58305 100644 --- a/app/screens/toast.js +++ b/app/screens/toast.js @@ -96,7 +96,7 @@ export function Toast({ case 'handleSwap': return [...baseStyle, styles.clipboardToast]; case 'error': - return [...baseStyle, styles.errorToast]; + return [...baseStyle, styles.clipboardToast]; case 'warning': return [...baseStyle, styles.warningToast]; case 'info': @@ -160,6 +160,15 @@ export function Toast({ theme && darkModeType ? COLORS.lightModeText : COLORS.primary } /> + ) : toast.type === 'error' ? ( + ) : ( )} @@ -201,7 +210,8 @@ export function Toast({ ) : ( diff --git a/context-store/activeAccount.js b/context-store/activeAccount.js index c6ecc979..903a991d 100644 --- a/context-store/activeAccount.js +++ b/context-store/activeAccount.js @@ -13,6 +13,7 @@ import { import { CUSTODY_ACCOUNTS_STORAGE_KEY, NWC_SECURE_STORE_MNEMOINC, + MAX_DERIVED_ACCOUNTS, } from '../app/constants'; import { useKeysContext } from './keys'; import { @@ -21,22 +22,33 @@ import { } from '../app/functions/handleMnemonic'; import { useGlobalContextProvider } from './context'; import { useAuthContext } from './authContext'; +import { deriveAccountMnemonic } from '../app/functions/accounts/derivedAccounts'; +import customUUID from '../app/functions/customUUID'; +import isValidMnemonic from '../app/functions/isValidMnemonic'; +import { useAppStatus } from './appStatus'; +import { useTranslation } from 'react-i18next'; // Create a context for the WebView ref const ActiveCustodyAccount = createContext(null); export const ActiveCustodyAccountProvider = ({ children }) => { - const { masterInfoObject } = useGlobalContextProvider(); + const { masterInfoObject, toggleMasterInfoObject } = + useGlobalContextProvider(); + const { didGetToHomepage } = useAppStatus(); const { authResetkey } = useAuthContext(); + const { t } = useTranslation(); const [custodyAccounts, setCustodyAccounts] = useState([]); const [isUsingNostr, setIsUsingNostr] = useState(false); const { accountMnemoinc } = useKeysContext(); const [nostrSeed, setNostrSeed] = useState(''); + const [activeDerivedMnemonic, setActiveDerivedMnemonic] = useState(null); const hasSessionReset = useRef(false); + const hasAutoRestoreCheckRun = useRef(false); const selectedAltAccount = custodyAccounts.filter(item => item.isActive); const didSelectAltAccount = !!selectedAltAccount.length; const isInitialRender = useRef(true); const enabledNWC = masterInfoObject.didViewNWCMessage; + const currentPins = masterInfoObject.pinnedAccounts || []; useEffect(() => { if (nostrSeed.length || !enabledNWC) return; @@ -122,6 +134,13 @@ export const ActiveCustodyAccountProvider = ({ children }) => { let newAccounts = accountInformation.filter(accounts => { return accounts.uuid !== account.uuid; }); + const isPinned = currentPins.includes(account.uuid); + if (isPinned) { + // clear from pinned list + toggleMasterInfoObject({ + pinnedAccounts: currentPins.filter(id => id !== account.uuid), + }); + } // clear spark information here too. Delte txs from database, reove listeners await setLocalStorageItem( CUSTODY_ACCOUNTS_STORAGE_KEY, @@ -140,7 +159,6 @@ export const ActiveCustodyAccountProvider = ({ children }) => { savedAccountInformation.push(accountInformation); - console.log(savedAccountInformation); await setLocalStorageItem( CUSTODY_ACCOUNTS_STORAGE_KEY, JSON.stringify(encriptAccountsList(savedAccountInformation)), @@ -183,6 +201,16 @@ export const ActiveCustodyAccountProvider = ({ children }) => { } else return { ...accounts, isActive: false }; }); + if (account.isActive && typeof account.derivationIndex === 'number') { + const derivedMnemonic = await deriveAccountMnemonic( + accountMnemoinc, + account.derivationIndex, + ); + setActiveDerivedMnemonic(derivedMnemonic); + } else { + setActiveDerivedMnemonic(null); + } + setCustodyAccounts(newAccounts); return { didWork: true }; } catch (err) { @@ -191,6 +219,244 @@ export const ActiveCustodyAccountProvider = ({ children }) => { } }; + const createDerivedAccount = async accountName => { + try { + const nextCloudIndex = masterInfoObject.nextAccountDerivationIndex || 3; + + const nextIndex = nextCloudIndex + 1; + + // Enforce hard cap to prevent overlap with gifts range (starts at index 1000) + if (nextIndex >= MAX_DERIVED_ACCOUNTS) { + return { + didWork: false, + error: `Maximum of ${MAX_DERIVED_ACCOUNTS} accounts reached. Please delete unused accounts.`, + }; + } + + // Don't store the mnemonic, just metadata + const accountInfo = { + uuid: customUUID(), + name: accountName, + derivationIndex: nextIndex, + dateCreated: Date.now(), + isActive: false, + accountType: 'derived', + profileEmoji: '', + }; + + await createAccount(accountInfo); + + // Update masterInfoObject with new index (automatically syncs to Firebase) + await toggleMasterInfoObject({ + nextAccountDerivationIndex: nextIndex, + }); + + return { didWork: true }; + } catch (err) { + console.log('Create derived account error', err); + return { didWork: false, error: err.message }; + } + }; + + const createImportedAccount = async (accountName, importedSeed) => { + try { + if (!importedSeed || typeof importedSeed !== 'string') { + return { didWork: false, error: 'Invalid seed provided' }; + } + + const words = importedSeed + .trim() + .toLowerCase() + .split(/\s+/) + .filter(Boolean); + if (words.length !== 12 || !isValidMnemonic(words)) { + return { + didWork: false, + error: 'Seed must be a valid 12-word recovery phrase', + }; + } + + const accountInfo = { + uuid: customUUID(), + name: accountName, + mnemoinc: words.join(' '), + dateCreated: Date.now(), + isActive: false, + accountType: 'imported', + profileEmoji: '', + }; + + await createAccount(accountInfo); + // NO cloud backup for imported accounts (contains sensitive seed) + return { didWork: true }; + } catch (err) { + console.log('Create imported account error', err); + return { didWork: false, error: err.message }; + } + }; + + const restoreDerivedAccount = async (accountName, derivationIndex) => { + try { + // Validation #1: Type check + if ( + typeof derivationIndex !== 'number' || + !Number.isInteger(derivationIndex) + ) { + return { + didWork: false, + error: 'Derivation index must be a whole number', + }; + } + + // Validation #2: Range check (minimum) + if (derivationIndex < 3) { + return { + didWork: false, + error: + 'Derivation index must be 3 or higher (indices 0-2 are reserved)', + }; + } + + // Validation #3: Range check (maximum - gifts boundary) + if (derivationIndex >= MAX_DERIVED_ACCOUNTS) { + return { + didWork: false, + error: `Derivation index must be less than ${MAX_DERIVED_ACCOUNTS} (gift wallet range)`, + }; + } + + // Validation #4: Check against nextAccountDerivationIndex + const nextCloudIndex = masterInfoObject.nextAccountDerivationIndex || 3; + if (derivationIndex > nextCloudIndex) { + return { + didWork: false, + error: `Cannot restore index ${derivationIndex}. Highest created account is ${ + nextCloudIndex - 1 + }`, + }; + } + + // Validation #5: Check if account already exists (idempotency) + const existingAccount = custodyAccounts.find( + acc => acc.derivationIndex === derivationIndex, + ); + if (existingAccount) { + return { + didWork: false, + error: `Account at index ${derivationIndex} already exists: "${existingAccount.name}"`, + }; + } + + // Create account with EXACT same structure as auto-restore + const accountInfo = { + uuid: customUUID(), + name: accountName, + derivationIndex: derivationIndex, + dateCreated: Date.now(), + isActive: false, + accountType: 'derived', + profileEmoji: '', + }; + + await createAccount(accountInfo); + + // CRITICAL: Do NOT update nextAccountDerivationIndex + // This is a restoration of an existing index, not a new sequential account + + return { didWork: true }; + } catch (err) { + console.log('Restore derived account error', err); + return { didWork: false, error: err.message }; + } + }; + + const getAccountMnemonic = async account => { + try { + if (!account) throw new Error('No account provided'); + // For derived accounts, re-derive on demand from main seed + if (account.derivationIndex !== undefined) { + const derivedMnemonic = await deriveAccountMnemonic( + accountMnemoinc, + account.derivationIndex, + ); + return derivedMnemonic; + } + // For imported accounts, return stored mnemonic + return account.mnemoinc; + } catch (err) { + console.log('Get account mnemonic error', err); + throw err; + } + }; + + const restoreDerivedAccountsFromCloud = async () => { + try { + // masterInfoObject is already loaded from Firebase by GlobalContextProvider + const nextIndex = Number( + masterInfoObject.nextAccountDerivationIndex || 3, + ); + + if (!nextIndex || nextIndex === 0) { + console.log('No derived accounts to restore'); + return { didWork: true, accountsRestored: 0 }; + } + + const existingDerivedIndexes = new Set( + custodyAccounts + .map(account => account.derivationIndex) + .filter(index => typeof index === 'number'), + ); + + const accountsToRestore = []; + for (let i = 4; i <= nextIndex; i++) { + if (existingDerivedIndexes.has(i)) continue; + accountsToRestore.push({ + uuid: customUUID(), + name: t('accountCard.fallbackAccountName', { index: i }), + derivationIndex: i, + dateCreated: Date.now(), + accountType: 'derived', + isActive: false, + profileEmoji: '', + }); + } + + if (accountsToRestore.length) { + const mergedAccounts = [...custodyAccounts, ...accountsToRestore]; + await setLocalStorageItem( + CUSTODY_ACCOUNTS_STORAGE_KEY, + JSON.stringify(encriptAccountsList(mergedAccounts)), + ); + setCustodyAccounts(mergedAccounts); + } + + console.log(`Restored ${accountsToRestore.length} derived account(s)`); + return { didWork: true, accountsRestored: accountsToRestore.length }; + } catch (err) { + console.log('Restore derived accounts error', err); + return { didWork: false, error: err.message }; + } + }; + + useEffect(() => { + async function restoreIfNeeded() { + const cloudIndex = masterInfoObject?.nextAccountDerivationIndex; + if ( + hasAutoRestoreCheckRun.current || + !accountMnemoinc || + custodyAccounts.length || + cloudIndex === undefined || + Number(cloudIndex) <= 0 || + !didGetToHomepage + ) { + return; + } + hasAutoRestoreCheckRun.current = true; + await restoreDerivedAccountsFromCloud(); + } + restoreIfNeeded(); + }, [accountMnemoinc, custodyAccounts, masterInfoObject, didGetToHomepage]); + useEffect(() => { if (isInitialRender.current) { isInitialRender.current = false; @@ -198,13 +464,21 @@ export const ActiveCustodyAccountProvider = ({ children }) => { } setNostrSeed(''); setIsUsingNostr(false); + setActiveDerivedMnemonic(null); setCustodyAccounts([]); hasSessionReset.current = false; + hasAutoRestoreCheckRun.current = false; }, [authResetkey]); const currentWalletMnemoinc = useMemo(() => { if (didSelectAltAccount) { - return selectedAltAccount[0].mnemoinc; + const activeAccount = selectedAltAccount[0]; + // For derived accounts, we'll need to derive the mnemonic + // But for backwards compatibility, check if mnemoinc exists first + if (activeAccount.mnemoinc) { + return activeAccount.mnemoinc; // Imported account + } + return activeDerivedMnemonic || accountMnemoinc; } else if (isUsingNostr) { return nostrSeed; } else { @@ -216,9 +490,52 @@ export const ActiveCustodyAccountProvider = ({ children }) => { didSelectAltAccount, isUsingNostr, nostrSeed, + activeDerivedMnemonic, ]); - const isUsingAltAccount = currentWalletMnemoinc !== accountMnemoinc; + const isUsingAltAccount = didSelectAltAccount || isUsingNostr; + + const custodyAccountsList = useMemo(() => { + return enabledNWC + ? [ + { + name: 'Main Wallet', + mnemoinc: accountMnemoinc, + accountType: 'main', + uuid: 'MW09xd09d8f0a9sf2n332', + }, + { + name: 'NWC', + mnemoinc: nostrSeed, + accountType: 'nwc', + uuid: 'NWC038rsd0f8234ajsf', + }, + ...custodyAccounts, + ] + : [ + { + name: 'Main Wallet', + mnemoinc: accountMnemoinc, + accountType: 'main', + uuid: 'MW09xd09d8f0a9sf2n332', + }, + ...custodyAccounts, + ]; + }, [accountMnemoinc, custodyAccounts, enabledNWC, nostrSeed]); + + const activeAccount = useMemo(() => { + const activeAltAccount = selectedAltAccount[0]; + return custodyAccountsList.find(account => { + const isMainWallet = account.name === 'Main Wallet'; + const isNWC = account.name === 'NWC'; + const isActive = isNWC + ? isUsingNostr + : isMainWallet + ? !activeAltAccount && !isUsingNostr + : activeAltAccount?.uuid === account.uuid; + return isActive; + }); + }, [custodyAccountsList, isUsingNostr, selectedAltAccount]); return ( { createAccount, updateAccount, updateAccountCacheOnly, + createDerivedAccount, + createImportedAccount, + restoreDerivedAccount, + getAccountMnemonic, + restoreDerivedAccountsFromCloud, selectedAltAccount, isUsingAltAccount, currentWalletMnemoinc, toggleIsUsingNostr, isUsingNostr, nostrSeed, + activeAccount, + custodyAccountsList, }} > {children} diff --git a/context-store/webViewContext.js b/context-store/webViewContext.js index d56c49d5..01b2f624 100644 --- a/context-store/webViewContext.js +++ b/context-store/webViewContext.js @@ -1180,8 +1180,8 @@ export const WebViewProvider = ({ children }) => { } const debouceID = setTimeout(() => { - forceReactNativeUse = true; - // startHandshake(); //remove this and app fully uses RN + // forceReactNativeUse = true; + startHandshake(); //remove this and app fully uses RN }, 250); return () => { diff --git a/db/interactionManager.js b/db/interactionManager.js index 465ad75b..7d2a360b 100644 --- a/db/interactionManager.js +++ b/db/interactionManager.js @@ -38,6 +38,7 @@ const PRESET_LOCAL_DATA = { defaultSpendToken: 'bitcoin', thousandsSeperator: 'space', enabledLiquidAutoSwap: true, + pinnedAccounts: [], }; async function sendDataToDB(newObject, uuid) { diff --git a/locales/de-DE/translation.json b/locales/de-DE/translation.json index 03853198..5614721c 100644 --- a/locales/de-DE/translation.json +++ b/locales/de-DE/translation.json @@ -897,7 +897,7 @@ "pool": "Pool", "couldNotFind": "Dieser Pool konnte nicht gefunden werden.", "createdBy": "Von ", - "moneyTransferred": "Geld auf Bitcoin-Guthaben übertragen", + "moneyTransferred": "Geld wurde in die Haupt-Wallet übertragen", "contribute": "Beitragen", "share": "Teilen", "noActivity": "Noch keine Aktivität. Sei der Erste, der beiträgt!", @@ -1341,11 +1341,13 @@ "header": "Sind Sie sicher?", "dataDeleteHeader": "Bitte wählen Sie die Daten aus, die von diesem Gerät gelöscht werden sollen.", "dataDeleteDesc": "Dies wird alle Wallet-Daten von Ihrem Gerät löschen. Wenn Sie keine Kopie Ihrer Seed-Phrase gespeichert haben, VERLIEREN Sie den Zugriff auf Ihre Gelder.", - "boxNotChecked": "Bitte markieren Sie das Kästchen, um zu bestätigen, dass Sie Ihre Wallet zurücksetzen möchten, bevor Sie fortfahren.", "seedAndPinOpt": "Alle Daten von meinem Gerät löschen", "balanceText": "Ihr Guthaben beträgt", "localStorageError": "Lokal gespeicherte Informationen konnten nicht gelöscht werden", - "secureStorageError": "Sicher gespeicherte Informationen konnten nicht gelöscht werden" + "secureStorageError": "Sicher gespeicherte Informationen konnten nicht gelöscht werden", + "boxNotChecked": "Bitte bestätige durch Ankreuzen, dass du deine Wallet löschen möchtest.", + "title": "Wallet löschen", + "warningText": "Ich verstehe, dass ich meine Gelder nur mit meiner Seed-Phrase wiederherstellen kann und dass sie nicht wiederhergestellt werden können, wenn ich sie verliere." }, "sparkInfo": { "title": "Wallet-Informationen", @@ -1528,8 +1530,67 @@ }, "viewAccountPage": { "informationMessage": "Sind Sie sicher, dass Sie diesen QR-Code anzeigen möchten?\n\nDas Scannen Ihrer Seed-Phrase ist praktisch, aber stellen Sie sicher, dass Sie ein sicheres und vertrauenswürdiges Gerät verwenden. Dies hilft, Ihre Wallet sicher zu halten." + }, + "editAccountName": { + "title": "Kontoname", + "namePlaceholder": "Name" + }, + "editAccountPage": { + "title": "Konto bearbeiten", + "accountNameLabel": "Kontoname", + "showRecoveryPhraseLabel": "Wiederherstellungsphrase anzeigen", + "removeAccountLabel": "Konto entfernen", + "activeAccountError": "Du kannst das derzeit aktive Konto nicht löschen. Bitte wechsle zu einem anderen Konto, bevor du dieses löschst.", + "account_pin": "Konto anheften", + "account_unpin": "Konto lösen" + }, + "selectCreateAccountType": { + "title": "Konto hinzufügen", + "createNewAccountTitle": "Neues Konto erstellen", + "createNewAccountDescription": "Wiederherstellbar über die Haupt-Seed-Phrase", + "importRecoveryPhraseTitle": "Wiederherstellungsphrase importieren", + "importRecoveryPhraseDescription": "Konten aus einer anderen Wallet importieren" + }, + "selectProfileImage": { + "title": "Avatar auswählen", + "searchPlaceholder": "Emojis suchen...", + "saveButton": "Avatar speichern" + }, + "restoreDerivedAccount": { + "title": "Konto wiederherstellen", + "emptyStateTitle": "Keine Konten zum Wiederherstellen", + "emptyStateMessage": "Alle Konten sind derzeit wiederhergestellt. Keine fehlenden Konten gefunden.", + "accountCardTitle": "Konto {{index}}", + "accountCardSubtitle": "Ableitungsindex: {{index}}", + "restoreButton": "Wiederherstellen", + "nameInputTitle": "Benenne dieses Konto", + "nameInputPlaceholder": "Konto {{index}}", + "nameInputConfirm": "Konto wiederherstellen", + "nameInputCancel": "Abbrechen", + "successMessage": "Konto {{index}} erfolgreich wiederhergestellt", + "errorMessage": "Fehler beim Wiederherstellen des Kontos" + }, + "selectAltAccount": { + "from": "Wähle das Konto, von dem du senden möchtest.", + "to": "Wähle das Konto, auf das du empfangen möchtest." + }, + "accountsPoolsScreen": { + "profileButton": "Profil", + "settingsButton": "Einstellungen", + "poolsTitle": "Pools", + "noPoolsMessage": "Du hast noch keine Pools erstellt. Tippe, um deinen ersten Pool zu erstellen.", + "yourAccountsTitle": "Deine Konten", + "addAccountButton": "Konto hinzufügen" } }, + "hub": { + "viewAll": "Alle anzeigen", + "accounts": "Konten", + "pinAccount": "Anheften", + "unpinAccount": "Lösen", + "maxPinsReached": "Maximal 2 angeheftete Konten. Löse zuerst eines.", + "morePoolsCount": "+{{count}} weitere" + }, "posPath": { "settings": { "nameTakenError": "Dieser Zahlungsterminal (POS)-Name ist bereits in Verwendung.", @@ -2033,6 +2094,11 @@ "giftCard": "{{name}} hat Ihnen eine {{giftCardName}} Geschenkkarte gesendet" } }, + + "accountCard": { + "fallbackAccountName": "Konto {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "Ihre Anfrage hat die Validierungsprüfung leider nicht bestanden.", "FSAG-1001": "In Ihrer Anfrage fehlt ein erforderliches Feld.", diff --git a/locales/en/translation.json b/locales/en/translation.json index a5992af1..6916f75e 100644 --- a/locales/en/translation.json +++ b/locales/en/translation.json @@ -914,7 +914,7 @@ "pool": "Pool", "couldNotFind": "We couldn't find this pool.", "createdBy": "By ", - "moneyTransferred": "Money transferred to Bitcoin Balance", + "moneyTransferred": "Money transferred to Main Wallet", "contribute": "Contribute", "share": "Share", "noActivity": "No activity yet. Be the first to contribute!", @@ -1358,7 +1358,15 @@ "qrWarning": "Are you sure you want to show this QR Code?\n\nScanning your seed phrase is convenient but be sure you're using a secure and trusted device. This helps keep your wallet safe.", "wordsText": "Words", "qrText": "QR Code", - "showSeedWarning": "Are you sure you want to show your seed phrase?" + "showSeedWarning": "Are you sure you want to show your seed phrase?", + "warning": { + "title": "Keep Your Recovery Phrase Secret", + "point1": "Your secret recovery phrase is like a master key to your wallet.", + "point2": "If someone gets it, they can steal your funds. There's no way to recover lost funds.", + "point3": "Never share it with anyone—no person, website, or app.", + "checkbox": "I understand that sharing my recovery phrase could result in permanent loss of funds.", + "continueButton": "Continue" + } }, "crashReporting": { "crashreporting_enabled": "Enabled crash reporting", @@ -1374,11 +1382,13 @@ "header": "Are you sure?", "dataDeleteHeader": "Select data to delete from this device.", "dataDeleteDesc": "This will delete all wallet data from your device. If you don’t have a copy of your seed phrase saved, you WILL lose access to your funds.", - "boxNotChecked": "Please check the box to confirm you want to reset your wallet before resetting.", "seedAndPinOpt": "Delete all data from my device", "balanceText": "Your balance is", "localStorageError": "Unable to delete locally stored information", - "secureStorageError": "Unable to delete securely stored information" + "secureStorageError": "Unable to delete securely stored information", + "boxNotChecked": "Please check the box to confirm you want to delete your wallet before deleating.", + "title": "Delete Wallet", + "warningText": "I understand that I can only recover my funds with my seed phrase, and if I lose it, my funds cannot be recovered." }, "sparkInfo": { "title": "Wallet Info", @@ -1565,8 +1575,77 @@ }, "viewAccountPage": { "informationMessage": "Are you sure you want to show this QR Code?\n\nScanning your seed phrase is convenient, but be sure you're using a secure and trusted device. This helps keep your wallet safe." + }, + "editAccountName": { + "title": "Account Name", + "namePlaceholder": "Name" + }, + "editAccountPage": { + "title": "Edit Account", + "accountNameLabel": "Account Name", + "showRecoveryPhraseLabel": "Show Recovery Phrase", + "removeAccountLabel": "Remove Account", + "activeAccountError": "You can’t delete the currently active account. Please switch to a different account before deleting this one.", + "account_pin": "Pin Account", + "account_unpin": "Unpin Account" + }, + "removeAccountPage": { + "title": "Remove", + "explanation_derived": "Even though you are removing this wallet from Blitz, you will be able to recover it using your seed phrase.", + "explanation_imported": "You are removing an imported wallet. You will only be able to recover this wallet by re-entering the seed phrase", + "cancelButton": "Cancel", + "removeButton": "Remove" + }, + "selectCreateAccountType": { + "title": "Add Account", + "createNewAccountTitle": "Create New Account", + "createNewAccountDescription": "Recoverable from main seed phrase", + "importRecoveryPhraseTitle": "Import Recovery Phrase", + "importRecoveryPhraseDescription": "Import accounts from another wallet", + "recoverRecoveryPhraseTitle": "Recover Account", + "recoverRecoveryPhraseDescription": "Restore an already created account" + }, + "selectProfileImage": { + "title": "Choose Avatar", + "searchPlaceholder": "Search emojis...", + "saveButton": "Save Avatar" + }, + "restoreDerivedAccount": { + "title": "Restore Account", + "emptyStateTitle": "No Accounts to Restore", + "emptyStateMessage": "All accounts are currently restored. No missing accounts found.", + "accountCardTitle": "Account {{index}}", + "accountCardSubtitle": "Derivation Index: {{index}}", + "restoreButton": "Restore", + "nameInputTitle": "Name This Account", + "nameInputPlaceholder": "Account {{index}}", + "nameInputConfirm": "Restore Account", + "nameInputCancel": "Cancel", + "successMessage": "Account {{index}} restored successfully", + "errorMessage": "Failed to restore account" + }, + "selectAltAccount": { + "from": "Select the account you want to send from.", + "to": "Select the account you want to receive to." } }, + "accountsPoolsScreen": { + "profileButton": "Profile", + "settingsButton": "Settings", + "poolsTitle": "Pools", + "noPoolsMessage": "You haven't created any pools yet. Tap to create your first pool.", + "noActivePools": "You have no active pools", + "yourAccountsTitle": "Your Accounts", + "addAccountButton": "Add Account" + }, + "hub": { + "viewAll": "View All", + "accounts": "Accounts", + "pinAccount": "Pin", + "unpinAccount": "Unpin", + "maxPinsReached": "Maximum 2 pinned accounts. Unpin one first.", + "morePoolsCount": "+{{count}} more" + }, "posPath": { "settings": { "nameTakenError": "This point-of-sale name is already in use.", @@ -1756,6 +1835,7 @@ "nostr": "nostr", "login mode": "Login Mode", "backup wallet": "Backup Wallet", + "show seed phrase": "Backup Wallet", "spark info": "Spark Info", "delete wallet": "Delete Wallet", "general": "General", @@ -2090,6 +2170,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Account {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "The request failed validation checks.", "FSAG-1001": "A required field is missing from your request.", diff --git a/locales/es/translation.json b/locales/es/translation.json index 1f8e5ade..9b079013 100644 --- a/locales/es/translation.json +++ b/locales/es/translation.json @@ -765,7 +765,7 @@ "pool": "Fondo", "couldNotFind": "No pudimos encontrar este fondo.", "createdBy": "Por ", - "moneyTransferred": "Fondos transferidos al saldo de Bitcoin", + "moneyTransferred": "Dinero transferido a la Wallet principal", "contribute": "Contribuir", "share": "Compartir", "noActivity": "Aún no hay actividad. ¡Sé el primero en contribuir!", @@ -1210,11 +1210,13 @@ "header": "¿Estás seguro?", "dataDeleteHeader": "Selecciona los datos a eliminar de este dispositivo.", "dataDeleteDesc": "Esto eliminará todos los datos de la billetera de tu dispositivo. Si no tienes una copia de tu frase semilla guardada, PERDERÁS el acceso a tus fondos.", - "boxNotChecked": "Por favor, marca la casilla para confirmar que deseas restablecer tu billetera antes de continuar.", "seedAndPinOpt": "Eliminar todos los datos de mi dispositivo", "balanceText": "Tu saldo es", "localStorageError": "No se pudo eliminar la información local", - "secureStorageError": "No se pudo eliminar la información segura" + "secureStorageError": "No se pudo eliminar la información segura", + "boxNotChecked": "Marca la casilla para confirmar que deseas eliminar tu wallet.", + "title": "Eliminar wallet", + "warningText": "Entiendo que solo puedo recuperar mis fondos con mi frase semilla y que, si la pierdo, no podrán recuperarse." }, "sparkInfo": { "title": "Información de billetera", @@ -1399,8 +1401,67 @@ }, "viewAccountPage": { "informationMessage": "¿Estás seguro de que quieres mostrar este código QR?\n\nEscanear tu semilla es conveniente, pero asegúrate de usar un dispositivo seguro y confiable. Esto ayuda a mantener tu billetera segura." + }, + "editAccountName": { + "title": "Nombre de la cuenta", + "namePlaceholder": "Nombre" + }, + "editAccountPage": { + "title": "Editar cuenta", + "accountNameLabel": "Nombre de la cuenta", + "showRecoveryPhraseLabel": "Mostrar frase de recuperación", + "removeAccountLabel": "Eliminar cuenta", + "activeAccountError": "No puedes eliminar la cuenta activa actualmente. Por favor, cambia a una cuenta diferente antes de eliminar esta.", + "account_pin": "Fijar cuenta", + "account_unpin": "Desfijar cuenta" + }, + "selectCreateAccountType": { + "title": "Agregar cuenta", + "createNewAccountTitle": "Crear nueva cuenta", + "createNewAccountDescription": "Recuperable desde la frase semilla principal", + "importRecoveryPhraseTitle": "Importar frase de recuperación", + "importRecoveryPhraseDescription": "Importar cuentas desde otra wallet" + }, + "selectProfileImage": { + "title": "Elegir avatar", + "searchPlaceholder": "Buscar emojis...", + "saveButton": "Guardar avatar" + }, + "restoreDerivedAccount": { + "title": "Restaurar cuenta", + "emptyStateTitle": "No hay cuentas para restaurar", + "emptyStateMessage": "Todas las cuentas están actualmente restauradas. No se encontraron cuentas faltantes.", + "accountCardTitle": "Cuenta {{index}}", + "accountCardSubtitle": "Índice de derivación: {{index}}", + "restoreButton": "Restaurar", + "nameInputTitle": "Nombra esta cuenta", + "nameInputPlaceholder": "Cuenta {{index}}", + "nameInputConfirm": "Restaurar cuenta", + "nameInputCancel": "Cancelar", + "successMessage": "Cuenta {{index}} restaurada exitosamente", + "errorMessage": "Error al restaurar la cuenta" + }, + "selectAltAccount": { + "from": "Selecciona la cuenta desde la que quieres enviar.", + "to": "Selecciona la cuenta en la que quieres recibir." + }, + "accountsPoolsScreen": { + "profileButton": "Perfil", + "settingsButton": "Configuración", + "poolsTitle": "Pools", + "noPoolsMessage": "Aún no has creado ningún pool. Toca para crear tu primer pool.", + "yourAccountsTitle": "Tus cuentas", + "addAccountButton": "Agregar cuenta" } }, + "hub": { + "viewAll": "Ver todo", + "accounts": "Cuentas", + "pinAccount": "Fijar", + "unpinAccount": "Desfijar", + "maxPinsReached": "Máximo 2 cuentas fijadas. Desfija una primero.", + "morePoolsCount": "+{{count}} más" + }, "posPath": { "settings": { "nameTakenError": "Este nombre de punto de venta ya está en uso.", @@ -1917,6 +1978,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Cuenta {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "La solicitud no superó las validaciones.", "FSAG-1001": "Falta un campo obligatorio en tu solicitud.", diff --git a/locales/fr/translation.json b/locales/fr/translation.json index 192d863d..03f54b4d 100644 --- a/locales/fr/translation.json +++ b/locales/fr/translation.json @@ -905,7 +905,7 @@ "pool": "Cagnotte", "couldNotFind": "Nous n’avons pas trouvé cette cagnotte.", "createdBy": "Par ", - "moneyTransferred": "Fonds transférés vers le solde Bitcoin", + "moneyTransferred": "Fonds transférés vers le portefeuille principal", "contribute": "Contribuer", "share": "Partager", "noActivity": "Aucune activité pour le moment. Soyez le premier à contribuer !", @@ -1354,11 +1354,13 @@ "header": "Êtes-vous sûr ?", "dataDeleteHeader": "Sélectionnez les données à supprimer de cet appareil.", "dataDeleteDesc": "Cette opération supprimera toutes les données du portefeuille de votre appareil. Si vous n'avez pas sauvegardé une copie de votre phrase de départ, vous PERDREZ l'accès à vos fonds.", - "boxNotChecked": "Veuillez cocher la case pour confirmer que vous souhaitez réinitialiser votre portefeuille avant de procéder à la réinitialisation.", "seedAndPinOpt": "Supprimer toutes les données de mon appareil", "balanceText": "Votre solde est de", "localStorageError": "Impossibilité de supprimer les informations stockées localement", - "secureStorageError": "Impossibilité de supprimer les informations stockées de manière sécurisée" + "secureStorageError": "Impossibilité de supprimer les informations stockées de manière sécurisée", + "boxNotChecked": "Veuillez cocher la case pour confirmer que vous souhaitez supprimer votre wallet.", + "title": "Supprimer le wallet", + "warningText": "Je comprends que je ne peux récupérer mes fonds qu’avec ma phrase seed et que si je la perds, mes fonds ne pourront pas être récupérés." }, "sparkInfo": { "title": "Informations sur le portefeuille", @@ -1541,8 +1543,67 @@ }, "viewAccountPage": { "informationMessage": "Êtes-vous sûr de vouloir afficher ce code QR ?\n\nScanner votre phrase de semence est pratique, mais assurez-vous d'utiliser un appareil sécurisé et de confiance. Cela permet de préserver la sécurité de votre portefeuille." + }, + "editAccountName": { + "title": "Nom du compte", + "namePlaceholder": "Nom" + }, + "editAccountPage": { + "title": "Modifier le compte", + "accountNameLabel": "Nom du compte", + "showRecoveryPhraseLabel": "Afficher la phrase de récupération", + "removeAccountLabel": "Supprimer le compte", + "activeAccountError": "Vous ne pouvez pas supprimer le compte actuellement actif. Veuillez passer à un autre compte avant de supprimer celui-ci.", + "account_pin": "Épingler le compte", + "account_unpin": "Désépingler le compte" + }, + "selectCreateAccountType": { + "title": "Ajouter un compte", + "createNewAccountTitle": "Créer un nouveau compte", + "createNewAccountDescription": "Récupérable via la phrase seed principale", + "importRecoveryPhraseTitle": "Importer une phrase de récupération", + "importRecoveryPhraseDescription": "Importer des comptes depuis un autre wallet" + }, + "selectProfileImage": { + "title": "Choisir un avatar", + "searchPlaceholder": "Rechercher des emojis...", + "saveButton": "Enregistrer l'avatar" + }, + "restoreDerivedAccount": { + "title": "Restaurer le compte", + "emptyStateTitle": "Aucun compte à restaurer", + "emptyStateMessage": "Tous les comptes sont actuellement restaurés. Aucun compte manquant trouvé.", + "accountCardTitle": "Compte {{index}}", + "accountCardSubtitle": "Indice de dérivation : {{index}}", + "restoreButton": "Restaurer", + "nameInputTitle": "Nommer ce compte", + "nameInputPlaceholder": "Compte {{index}}", + "nameInputConfirm": "Restaurer le compte", + "nameInputCancel": "Annuler", + "successMessage": "Compte {{index}} restauré avec succès", + "errorMessage": "Échec de la restauration du compte" + }, + "selectAltAccount": { + "from": "Sélectionnez le compte depuis lequel vous souhaitez envoyer.", + "to": "Sélectionnez le compte sur lequel vous souhaitez recevoir." + }, + "accountsPoolsScreen": { + "profileButton": "Profil", + "settingsButton": "Paramètres", + "poolsTitle": "Pools", + "noPoolsMessage": "Vous n’avez pas encore créé de pool. Appuyez pour créer votre premier pool.", + "yourAccountsTitle": "Vos comptes", + "addAccountButton": "Ajouter un compte" } }, + "hub": { + "viewAll": "Tout voir", + "accounts": "Comptes", + "pinAccount": "Épingler", + "unpinAccount": "Désépingler", + "maxPinsReached": "Maximum 2 comptes épinglés. Désépinglez-en un d'abord.", + "morePoolsCount": "+{{count}} de plus" + }, "posPath": { "settings": { "nameTakenError": "Ce nom de point de vente est déjà utilisé.", @@ -2050,6 +2111,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Compte {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "La requête n’a pas passé les vérifications de validation.", "FSAG-1001": "Un champ requis est manquant dans votre requête.", diff --git a/locales/it/translation.json b/locales/it/translation.json index 8500091d..c5e4fea8 100644 --- a/locales/it/translation.json +++ b/locales/it/translation.json @@ -916,7 +916,7 @@ "pool": "Pool", "couldNotFind": "Non siamo riusciti a trovare questo pool.", "createdBy": "Da ", - "moneyTransferred": "Fondi trasferiti al saldo Bitcoin", + "moneyTransferred": "Fondi trasferiti al wallet principale", "contribute": "Contribuisci", "share": "Condividi", "noActivity": "Nessuna attività al momento. Sii il primo a contribuire!", @@ -1372,11 +1372,14 @@ "header": "Sei sicuro?", "dataDeleteHeader": "Seleziona i dati da eliminare da questo dispositivo.", "dataDeleteDesc": "Questo eliminerà tutti i dati del portafoglio dal tuo dispositivo. Se non hai una copia della tua frase seed salvata, PERDERAI l’accesso ai tuoi fondi.", - "boxNotChecked": "Seleziona la casella per confermare che desideri reimpostare il tuo portafoglio prima di continuare.", + "seedAndPinOpt": "Elimina tutti i dati dal mio dispositivo", "balanceText": "Il tuo saldo è", "localStorageError": "Impossibile eliminare le informazioni memorizzate localmente", - "secureStorageError": "Impossibile eliminare le informazioni memorizzate in modo sicuro" + "secureStorageError": "Impossibile eliminare le informazioni memorizzate in modo sicuro", + "boxNotChecked": "Seleziona la casella per confermare che vuoi eliminare il wallet.", + "title": "Elimina wallet", + "warningText": "Comprendo che posso recuperare i miei fondi solo con la mia frase seed e che, se la perdo, i miei fondi non potranno essere recuperati." }, "sparkInfo": { "title": "Info Wallet", @@ -1565,8 +1568,67 @@ }, "viewAccountPage": { "informationMessage": "Sei sicuro di voler mostrare questo QR Code?\n\nScansionare il tuo seed è comodo, ma assicurati di usare un dispositivo sicuro e affidabile. Questo aiuta a mantenere il tuo wallet sicuro." + }, + "editAccountName": { + "title": "Nome account", + "namePlaceholder": "Nome" + }, + "editAccountPage": { + "title": "Modifica account", + "accountNameLabel": "Nome account", + "showRecoveryPhraseLabel": "Mostra frase di recupero", + "removeAccountLabel": "Rimuovi account", + "activeAccountError": "Non puoi eliminare l'account attualmente attivo. Passa a un altro account prima di eliminare questo.", + "account_pin": "Fissa account", + "account_unpin": "Rimuovi fissaggio" + }, + "selectCreateAccountType": { + "title": "Aggiungi account", + "createNewAccountTitle": "Crea nuovo account", + "createNewAccountDescription": "Recuperabile dalla frase seed principale", + "importRecoveryPhraseTitle": "Importa frase di recupero", + "importRecoveryPhraseDescription": "Importa account da un altro wallet" + }, + "selectProfileImage": { + "title": "Scegli avatar", + "searchPlaceholder": "Cerca emoji...", + "saveButton": "Salva avatar" + }, + "restoreDerivedAccount": { + "title": "Ripristina account", + "emptyStateTitle": "Nessun account da ripristinare", + "emptyStateMessage": "Tutti gli account sono attualmente ripristinati. Nessun account mancante trovato.", + "accountCardTitle": "Account {{index}}", + "accountCardSubtitle": "Indice di derivazione: {{index}}", + "restoreButton": "Ripristina", + "nameInputTitle": "Nomina questo account", + "nameInputPlaceholder": "Account {{index}}", + "nameInputConfirm": "Ripristina account", + "nameInputCancel": "Annulla", + "successMessage": "Account {{index}} ripristinato con successo", + "errorMessage": "Impossibile ripristinare l'account" + }, + "selectAltAccount": { + "from": "Seleziona l’account da cui vuoi inviare.", + "to": "Seleziona l’account su cui vuoi ricevere." + }, + "accountsPoolsScreen": { + "profileButton": "Profilo", + "settingsButton": "Impostazioni", + "poolsTitle": "Pool", + "noPoolsMessage": "Non hai ancora creato alcun pool. Tocca per creare il tuo primo pool.", + "yourAccountsTitle": "I tuoi account", + "addAccountButton": "Aggiungi account" } }, + "hub": { + "viewAll": "Vedi tutto", + "accounts": "Account", + "pinAccount": "Fissa", + "unpinAccount": "Rimuovi", + "maxPinsReached": "Massimo 2 account fissati. Rimuovine uno prima.", + "morePoolsCount": "+{{count}} altri" + }, "posPath": { "settings": { "nameTakenError": "Questo nome terminale è già in uso.", @@ -2094,6 +2156,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Account {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "La richiesta non ha superato i controlli di validazione.", "FSAG-1001": "Manca un campo obbligatorio nella richiesta.", diff --git a/locales/pt-BR/translation.json b/locales/pt-BR/translation.json index a31771c5..ab545a3f 100644 --- a/locales/pt-BR/translation.json +++ b/locales/pt-BR/translation.json @@ -916,7 +916,7 @@ "pool": "Pool", "couldNotFind": "Não foi possível encontrar este pool.", "createdBy": "Por ", - "moneyTransferred": "Fundos transferidos para o saldo de Bitcoin", + "moneyTransferred": "Fundos transferidos para a Wallet principal", "contribute": "Contribuir", "share": "Compartilhar", "noActivity": "Nenhuma atividade ainda. Seja o primeiro a contribuir!", @@ -1374,11 +1374,13 @@ "header": "Tem certeza?", "dataDeleteHeader": "Selecione os dados a serem excluídos deste dispositivo.", "dataDeleteDesc": "Isso excluirá todos os dados da carteira do seu dispositivo. Se você não tiver uma cópia da sua frase-semente salva, PERDERÁ o acesso aos seus fundos.", - "boxNotChecked": "Marque a caixa para confirmar que deseja redefinir sua carteira antes de continuar.", "seedAndPinOpt": "Excluir todos os dados do meu dispositivo", "balanceText": "Seu saldo é", "localStorageError": "Não foi possível excluir as informações armazenadas localmente", - "secureStorageError": "Não foi possível excluir as informações armazenadas com segurança" + "secureStorageError": "Não foi possível excluir as informações armazenadas com segurança", + "boxNotChecked": "Marque a caixa para confirmar que deseja excluir sua wallet.", + "title": "Excluir wallet", + "warningText": "Entendo que só posso recuperar meus fundos com minha frase seed e que, se eu perdê-la, meus fundos não poderão ser recuperados." }, "sparkInfo": { "title": "Informações da carteira", @@ -1565,8 +1567,67 @@ }, "viewAccountPage": { "informationMessage": "Tem certeza de que deseja mostrar este códdigo QR?\n\nLer sua frase-semente é conveniente, mas certifique-se de estar usando um dispositivo seguro e confiável. Isso ajuda a manter sua carteira segura." + }, + "editAccountName": { + "title": "Nome da conta", + "namePlaceholder": "Nome" + }, + "editAccountPage": { + "title": "Editar conta", + "accountNameLabel": "Nome da conta", + "showRecoveryPhraseLabel": "Mostrar frase de recuperação", + "removeAccountLabel": "Remover conta", + "activeAccountError": "Você não pode excluir a conta ativa no momento. Por favor, mude para uma conta diferente antes de excluir esta.", + "account_pin": "Fixar conta", + "account_unpin": "Desafixar conta" + }, + "selectCreateAccountType": { + "title": "Adicionar conta", + "createNewAccountTitle": "Criar nova conta", + "createNewAccountDescription": "Recuperável pela frase seed principal", + "importRecoveryPhraseTitle": "Importar frase de recuperação", + "importRecoveryPhraseDescription": "Importar contas de outra wallet" + }, + "selectProfileImage": { + "title": "Escolher avatar", + "searchPlaceholder": "Buscar emojis...", + "saveButton": "Salvar avatar" + }, + "restoreDerivedAccount": { + "title": "Restaurar conta", + "emptyStateTitle": "Nenhuma conta para restaurar", + "emptyStateMessage": "Todas as contas estão atualmente restauradas. Nenhuma conta ausente encontrada.", + "accountCardTitle": "Conta {{index}}", + "accountCardSubtitle": "Índice de derivação: {{index}}", + "restoreButton": "Restaurar", + "nameInputTitle": "Nomeie esta conta", + "nameInputPlaceholder": "Conta {{index}}", + "nameInputConfirm": "Restaurar conta", + "nameInputCancel": "Cancelar", + "successMessage": "Conta {{index}} restaurada com sucesso", + "errorMessage": "Falha ao restaurar a conta" + }, + "selectAltAccount": { + "from": "Selecione a conta da qual deseja enviar.", + "to": "Selecione a conta para a qual deseja receber." + }, + "accountsPoolsScreen": { + "profileButton": "Perfil", + "settingsButton": "Configurações", + "poolsTitle": "Pools", + "noPoolsMessage": "Você ainda não criou nenhum pool. Toque para criar seu primeiro pool.", + "yourAccountsTitle": "Suas contas", + "addAccountButton": "Adicionar conta" } }, + "hub": { + "viewAll": "Ver tudo", + "accounts": "Contas", + "pinAccount": "Fixar", + "unpinAccount": "Desafixar", + "maxPinsReached": "Máximo de 2 contas fixadas. Desafixe uma primeiro.", + "morePoolsCount": "+{{count}} mais" + }, "posPath": { "settings": { "nameTakenError": "Este nome de ponto de venda já está em uso.", @@ -2093,6 +2154,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Conta {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "A solicitação não passou pelas validações.", "FSAG-1001": "Um campo obrigatório está ausente na solicitação.", diff --git a/locales/ru/translation.json b/locales/ru/translation.json index ee271df9..2afb3356 100644 --- a/locales/ru/translation.json +++ b/locales/ru/translation.json @@ -902,7 +902,7 @@ "pool": "Пул", "couldNotFind": "Мы не смогли найти этот пул.", "createdBy": "От ", - "moneyTransferred": "Средства переведены на Bitcoin-баланс", + "moneyTransferred": "Средства переведены в основной кошелёк", "contribute": "Внести вклад", "share": "Поделиться", "noActivity": "Пока нет активности. Будьте первым, кто внесёт вклад!", @@ -1351,11 +1351,14 @@ "header": "Вы уверены?", "dataDeleteHeader": "Выберите данные для удаления.", "dataDeleteDesc": "Это удалит все данные кошелька. Без сид-фразы вы потеряете доступ к средствам.", - "boxNotChecked": "Поставьте галочку для подтверждения.", + "seedAndPinOpt": "Удалить все данные с устройства", "balanceText": "Ваш баланс", "localStorageError": "Не удалось удалить локальные данные", - "secureStorageError": "Не удалось удалить защищенные данные" + "secureStorageError": "Не удалось удалить защищенные данные", + "boxNotChecked": "Отметьте галочку, чтобы подтвердить удаление кошелька.", + "title": "Удалить кошелёк", + "warningText": "Я понимаю, что могу восстановить средства только с помощью своей seed-фразы, и если я её потеряю, средства нельзя будет восстановить." }, "sparkInfo": { "title": "Инфо о кошельке", @@ -1538,8 +1541,67 @@ }, "viewAccountPage": { "informationMessage": "Показать QR-код?\n\nСканируйте только на безопасном устройстве." + }, + "editAccountName": { + "title": "Название аккаунта", + "namePlaceholder": "Название" + }, + "editAccountPage": { + "title": "Редактировать аккаунт", + "accountNameLabel": "Название аккаунта", + "showRecoveryPhraseLabel": "Показать фразу восстановления", + "removeAccountLabel": "Удалить аккаунт", + "activeAccountError": "Вы не можете удалить текущий активный аккаунт. Пожалуйста, переключитесь на другой аккаунт перед удалением этого.", + "account_pin": "Закрепить аккаунт", + "account_unpin": "Открепить аккаунт" + }, + "selectCreateAccountType": { + "title": "Добавить аккаунт", + "createNewAccountTitle": "Создать новый аккаунт", + "createNewAccountDescription": "Восстанавливается из основной seed-фразы", + "importRecoveryPhraseTitle": "Импортировать фразу восстановления", + "importRecoveryPhraseDescription": "Импортировать аккаунты из другого кошелька" + }, + "selectProfileImage": { + "title": "Выбрать аватар", + "searchPlaceholder": "Поиск эмодзи...", + "saveButton": "Сохранить аватар" + }, + "restoreDerivedAccount": { + "title": "Восстановить аккаунт", + "emptyStateTitle": "Нет аккаунтов для восстановления", + "emptyStateMessage": "Все аккаунты в настоящее время восстановлены. Отсутствующих аккаунтов не найдено.", + "accountCardTitle": "Аккаунт {{index}}", + "accountCardSubtitle": "Индекс деривации: {{index}}", + "restoreButton": "Восстановить", + "nameInputTitle": "Назовите этот аккаунт", + "nameInputPlaceholder": "Аккаунт {{index}}", + "nameInputConfirm": "Восстановить аккаунт", + "nameInputCancel": "Отмена", + "successMessage": "Аккаунт {{index}} успешно восстановлен", + "errorMessage": "Не удалось восстановить аккаунт" + }, + "selectAltAccount": { + "from": "Выберите аккаунт, с которого хотите отправить.", + "to": "Выберите аккаунт, на который хотите получить." + }, + "accountsPoolsScreen": { + "profileButton": "Профиль", + "settingsButton": "Настройки", + "poolsTitle": "Пулы", + "noPoolsMessage": "Вы ещё не создали ни одного пула. Нажмите, чтобы создать первый пул.", + "yourAccountsTitle": "Ваши аккаунты", + "addAccountButton": "Добавить аккаунт" } }, + "hub": { + "viewAll": "Показать все", + "accounts": "Аккаунты", + "pinAccount": "Закрепить", + "unpinAccount": "Открепить", + "maxPinsReached": "Максимум 2 закреплённых аккаунта. Сначала открепите один.", + "morePoolsCount": "+{{count}} ещё" + }, "posPath": { "settings": { "nameTakenError": "Имя терминала занято.", @@ -2056,6 +2118,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Аккаунт {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "Запрос не прошел валидацию.", "FSAG-1001": "Отсутствует обязательное поле.", diff --git a/locales/sv/translation.json b/locales/sv/translation.json index 2d0021e2..e134edf7 100644 --- a/locales/sv/translation.json +++ b/locales/sv/translation.json @@ -905,7 +905,7 @@ "pool": "Pool", "couldNotFind": "Vi kunde inte hitta denna pool.", "createdBy": "Av ", - "moneyTransferred": "Pengar överförda till Bitcoin-saldo", + "moneyTransferred": "Pengar överförda till huvudplånboken", "contribute": "Bidra", "share": "Dela", "noActivity": "Ingen aktivitet ännu. Bli den första att bidra!", @@ -1354,11 +1354,13 @@ "header": "Är du säker?", "dataDeleteHeader": "Välj data som ska raderas från den här enheten.", "dataDeleteDesc": "Detta kommer att radera alla plånboksdata från din enhet. Om du inte har sparat en kopia av din seed-fras kommer du att förlora tillgången till dina pengar.", - "boxNotChecked": "Markera rutan för att bekräfta att du vill återställa din plånbok innan du återställer.", "seedAndPinOpt": "Ta bort alla data från min enhet", "balanceText": "Ditt saldo är", "localStorageError": "Kan inte radera lokalt lagrad information", - "secureStorageError": "Kan inte radera säkert lagrad information" + "secureStorageError": "Kan inte radera säkert lagrad information", + "boxNotChecked": "Markera rutan för att bekräfta att du vill ta bort din wallet.", + "title": "Ta bort wallet", + "warningText": "Jag förstår att jag endast kan återställa mina medel med min seed-fras och att om jag förlorar den kan mina medel inte återställas." }, "sparkInfo": { "title": "Information om plånboken", @@ -1541,8 +1543,67 @@ }, "viewAccountPage": { "informationMessage": "Är du säker på att du vill visa den här QR-koden?\n\nAtt skanna din seed-fras är bekvämt, men se till att du använder en säker och betrodd enhet. Detta hjälper till att hålla din plånbok säker." + }, + "editAccountName": { + "title": "Kontonamn", + "namePlaceholder": "Namn" + }, + "editAccountPage": { + "title": "Redigera konto", + "accountNameLabel": "Kontonamn", + "showRecoveryPhraseLabel": "Visa återställningsfras", + "removeAccountLabel": "Ta bort konto", + "activeAccountError": "Du kan inte ta bort det för närvarande aktiva kontot. Vänligen byt till ett annat konto innan du tar bort detta.", + "account_pin": "Fäst konto", + "account_unpin": "Lossa konto" + }, + "selectCreateAccountType": { + "title": "Lägg till konto", + "createNewAccountTitle": "Skapa nytt konto", + "createNewAccountDescription": "Kan återställas från huvudets seed-fras", + "importRecoveryPhraseTitle": "Importera återställningsfras", + "importRecoveryPhraseDescription": "Importera konton från en annan wallet" + }, + "selectProfileImage": { + "title": "Välj avatar", + "searchPlaceholder": "Sök emojis...", + "saveButton": "Spara avatar" + }, + "restoreDerivedAccount": { + "title": "Återställ konto", + "emptyStateTitle": "Inga konton att återställa", + "emptyStateMessage": "Alla konton är för närvarande återställda. Inga saknade konton hittades.", + "accountCardTitle": "Konto {{index}}", + "accountCardSubtitle": "Derivationsindex: {{index}}", + "restoreButton": "Återställ", + "nameInputTitle": "Namnge detta konto", + "nameInputPlaceholder": "Konto {{index}}", + "nameInputConfirm": "Återställ konto", + "nameInputCancel": "Avbryt", + "successMessage": "Konto {{index}} återställdes framgångsrikt", + "errorMessage": "Misslyckades återställa kontot" + }, + "selectAltAccount": { + "from": "Välj kontot du vill skicka från.", + "to": "Välj kontot du vill ta emot till." + }, + "accountsPoolsScreen": { + "profileButton": "Profil", + "settingsButton": "Inställningar", + "poolsTitle": "Pools", + "noPoolsMessage": "Du har inte skapat några pools ännu. Tryck för att skapa din första pool.", + "yourAccountsTitle": "Dina konton", + "addAccountButton": "Lägg till konto" } }, + "hub": { + "viewAll": "Visa alla", + "accounts": "Konton", + "pinAccount": "Fäst", + "unpinAccount": "Lossa", + "maxPinsReached": "Maximalt 2 fästa konton. Lossa ett först.", + "morePoolsCount": "+{{count}} till" + }, "posPath": { "settings": { "nameTakenError": "Detta namn på försäljningsstället används redan.", @@ -2050,6 +2111,10 @@ } }, + "accountCard": { + "fallbackAccountName": "Konto {{index}}" + }, + "flashnetUserMessages": { "FSAG-1000": "Begäran klarade inte valideringskontrollerna.", "FSAG-1001": "Ett obligatoriskt fält saknas i begäran.", diff --git a/navigation/screens.js b/navigation/screens.js index aa0c089c..875452b2 100644 --- a/navigation/screens.js +++ b/navigation/screens.js @@ -44,7 +44,14 @@ import { } from '../app/components/admin/homeComponents/settingsContent'; import AccountPaymentPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/accountPaymentPage'; import CreateCustodyAccountPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/createAccountPage'; +import SelectCreateAccountType from '../app/components/admin/homeComponents/settingsContent/accountComponents/selectCreateAccountType'; +import EditAccountPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/editAccountPage'; +import EditAccountName from '../app/components/admin/homeComponents/settingsContent/accountComponents/editAccountName'; +import EmojiAvatarSelector from '../app/components/admin/homeComponents/settingsContent/accountComponents/selectProfileImage'; +import RemoveAccountPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/removeAccountPage'; +import RestoreDerivedAccountPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/restoreDerivedAccountPage'; import ViewCustodyAccountPage from '../app/components/admin/homeComponents/settingsContent/accountComponents/viewAccountPage'; +import SeedPhraseWarning from '../app/components/admin/homeComponents/settingsContent/seedPhraseWarning'; import ConfirmPinForLoginMode from '../app/components/admin/homeComponents/settingsContent/loginSecurity/enterPinPage'; import Nip5VerificationPage from '../app/components/admin/homeComponents/settingsContent/nip5/nip5Account'; import CreateNostrConnectAccount from '../app/components/admin/homeComponents/settingsContent/nwc/createNWCAccount'; @@ -83,6 +90,7 @@ import { TechnicalTransactionDetails, ViewAllTxPage, SwapsPage, + SettingsHub, } from '../app/screens/inAccount'; import ConversionHistory from '../app/components/admin/homeComponents/swaps/swapHistory'; @@ -115,7 +123,8 @@ const SLIDE_FROM_RIGHT_SCREENS = [ component: CreateAccountHome, options: { gestureEnabled: true }, }, - { name: 'SettingsHome', component: SettingsIndex }, + { name: 'SettingsHome', component: SettingsHub }, + { name: 'ShowProfileQrSlideRight', component: ShowProfileQr }, // {name: 'HistoricalOnChainPayments', component: HistoricalOnChainPayments}, { name: 'ChooseContactHalfModal', component: ChooseContactHalfModal }, { name: 'SettingsContentHome', component: SettingsContentIndex }, @@ -153,7 +162,14 @@ const SLIDE_FROM_RIGHT_SCREENS = [ // {name: 'EcashSettings', component: EcashSettings}, { name: 'AddPOSItemsPage', component: AddPOSItemsPage }, { name: 'CreateCustodyAccount', component: CreateCustodyAccountPage }, + { name: 'SelectCreateAccountType', component: SelectCreateAccountType }, + { name: 'RestoreDerivedAccount', component: RestoreDerivedAccountPage }, + { name: 'EditAccountName', component: EditAccountName }, { name: 'ViewCustodyAccount', component: ViewCustodyAccountPage }, + { name: 'EditAccountPage', component: EditAccountPage }, + { name: 'RemoveAccountPage', component: RemoveAccountPage }, + { name: 'SeedPhraseWarning', component: SeedPhraseWarning }, + { name: 'EmojiAvatarSelector', component: EmojiAvatarSelector }, { name: 'CustodyAccountPaymentPage', component: AccountPaymentPage }, { name: 'NosterWalletConnect', component: NosterWalletConnect }, { name: 'CreateNostrConnectAccount', component: CreateNostrConnectAccount },