diff --git a/src/assets/CreateNewWalletGroup.png b/src/assets/CreateNewWalletGroup.png new file mode 100644 index 00000000000..13a91c373e5 Binary files /dev/null and b/src/assets/CreateNewWalletGroup.png differ diff --git a/src/components/sheet/SlackSheet.tsx b/src/components/sheet/SlackSheet.tsx index 27212d0527c..0fc473c1835 100644 --- a/src/components/sheet/SlackSheet.tsx +++ b/src/components/sheet/SlackSheet.tsx @@ -45,7 +45,8 @@ const Container = styled(Centered).attrs({ ? deviceHeight - contentHeight : 0, }), - ...(IS_ANDROID ? { borderTopLeftRadius: 30, borderTopRightRadius: 30 } : {}), + borderTopLeftRadius: 30, + borderTopRightRadius: 30, backgroundColor: backgroundColor, bottom: 0, left: 0, diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 5865734fece..c7678568df5 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2650,6 +2650,14 @@ "my_account_address": "My account address:", "network_title": "Network", "new": { + "choose_wallet_group": { + "title": "Choose Wallet Group", + "description": "Choose the wallet group you’d like to create a new wallet within, or create a new one." + }, + "new_wallet_group": { + "title": "New Wallet Group", + "description": "Create a new wallet group" + }, "add_wallet_sheet": { "options": { "cloud": { diff --git a/src/navigation/AddWalletNavigator.tsx b/src/navigation/AddWalletNavigator.tsx index 30b617d7bbf..92b6cb7bc7c 100644 --- a/src/navigation/AddWalletNavigator.tsx +++ b/src/navigation/AddWalletNavigator.tsx @@ -7,6 +7,7 @@ import { ImportOrWatchWalletSheet, ImportOrWatchWalletSheetParams } from '@/scre import { BackgroundProvider } from '@/design-system'; import { RouteProp, useRoute } from '@react-navigation/native'; import { SimpleSheet } from '@/components/sheet/SimpleSheet'; +import { ChooseWalletGroup } from './ChooseWalletGroup'; const Swipe = createMaterialTopTabNavigator(); diff --git a/src/navigation/ChooseWalletGroup.tsx b/src/navigation/ChooseWalletGroup.tsx new file mode 100644 index 00000000000..d768d071b23 --- /dev/null +++ b/src/navigation/ChooseWalletGroup.tsx @@ -0,0 +1,240 @@ +import React from 'react'; +import { Separator, Text, useBackgroundColor, useForegroundColor } from '@/design-system'; +import { View, Text as NativeText } from 'react-native'; +import chroma from 'chroma-js'; +import { useInitializeWallet, useWallets } from '@/hooks'; +import { PROFILES, useExperimentalFlag } from '@/config'; +import { useDispatch } from 'react-redux'; +import Routes from '@/navigation/routesNames'; +import { useNavigation } from './Navigation'; +import WalletBackupTypes from '@/helpers/walletBackupTypes'; +import WalletTypes from '@/helpers/walletTypes'; +import { backupUserDataIntoCloud } from '@/handlers/cloudBackup'; +import { logger, RainbowError } from '@/logger'; +import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; +import { createWallet, RainbowAccount, RainbowWallet } from '@/model/wallet'; +import { ButtonPressAnimation } from '@/components/animations'; +import { abbreviateEnsForDisplay, formatAddressForDisplay } from '@/utils/abbreviations'; +import { ImgixImage } from '@/components/images'; +import { useTheme } from '@/theme'; +import { removeFirstEmojiFromString, returnStringFirstEmoji } from '@/helpers/emojiHandler'; +import { profileUtils } from '@/utils'; +import * as i18n from '@/languages'; +import showWalletErrorAlert from '@/helpers/support'; +import { ScrollView } from 'react-native-gesture-handler'; +import CreateNewWalletGroupIcon from '@/assets/CreateNewWalletGroup.png'; +import { SlackSheet } from '@/components/sheet'; + +function NewWalletGroup({ numWalletGroups }: { numWalletGroups: number }) { + const blue = useForegroundColor('blue'); + + const { navigate } = useNavigation(); + const profilesEnabled = useExperimentalFlag(PROFILES); + const dispatch = useDispatch(); + const initializeWallet = useInitializeWallet(); + + const onNewWalletGroup = () => { + navigate(Routes.MODAL_SCREEN, { + actionType: 'Create', + numWalletGroups, + onCloseModal: async (args: { name: string; color: number }) => { + if (!args) return; + try { + const { name, color } = args; + await createWallet({ color, name }); + await dispatch(walletsLoadState(profilesEnabled)); + // @ts-ignore + await initializeWallet(); + navigate(Routes.WALLET_SCREEN, {}, true); + } catch (error) { + logger.error(new RainbowError('[AddWalletSheet]: Error while trying to add account'), { error }); + } + }, + profile: { color: null, name: `` }, + type: 'new_wallet_group', + }); + }; + + return ( + + + + + {i18n.t(i18n.l.wallet.new.new_wallet_group.title)} + + + {i18n.t(i18n.l.wallet.new.new_wallet_group.description)} + + + + ); +} + +function AccountAvatar({ account, size }: { account: RainbowAccount; size: number }) { + const { colors } = useTheme(); + + if (account.image) { + return ; + } + + const backgroundColor = colors.avatarBackgrounds[account.color]; + const emoji = returnStringFirstEmoji(account.label) || profileUtils.addressHashedEmoji(account.address); + + return ( + + {emoji} + + ); +} + +function WalletGroup({ wallet }: { wallet: RainbowWallet }) { + const separatorSecondary = useForegroundColor('separatorSecondary'); + + const accounts = wallet.addresses; + + const { navigate } = useNavigation(); + const dispatch = useDispatch(); + const initializeWallet = useInitializeWallet(); + + const onAddToGroup = () => { + navigate(Routes.MODAL_SCREEN, { + actionType: 'Create', + asset: [], + isNewProfile: true, + onCloseModal: async (args: { name: string; color: number }) => { + if (!args) return; + try { + const { name, color } = args; + if (wallet.damaged) throw new Error('Wallet is damaged'); + const newWallets = await dispatch(createAccountForWallet(wallet.id, color, name)); + // @ts-ignore + await initializeWallet(); + // If this wallet was previously backed up to the cloud + // We need to update userData backup so it can be restored too + if (wallet.backedUp && wallet.backupType === WalletBackupTypes.cloud) { + try { + await backupUserDataIntoCloud({ wallets: newWallets }); + } catch (error) { + logger.error(new RainbowError('[AddWalletSheet]: Updating wallet userdata failed after new account creation'), { error }); + throw error; + } + } + navigate(Routes.WALLET_SCREEN, {}, true); + } catch (e) { + logger.error(new RainbowError('[AddWalletSheet]: Error while trying to add account'), { + error: e, + }); + showWalletErrorAlert(); + } + }, + profile: { color: null, name: `` }, + type: 'wallet_profile', + }); + }; + + return ( + + + {accounts.slice(0, 4).map(account => ( + + ))} + + + + {removeFirstEmojiFromString(wallet.name)} + + + + {abbreviateEnsForDisplay(accounts[0].ens) || formatAddressForDisplay(accounts[0].address, 4, 4)} + + {accounts.length > 1 && ( + + + {`+${accounts.length - 1}`} + + + )} + + + + ); +} + +export function ChooseWalletGroup() { + const { goBack } = useNavigation(); + const { wallets } = useWallets(); + const surfaceSecondary = useBackgroundColor('surfaceSecondary'); + + if (!wallets) { + showWalletErrorAlert(); + return; + } + + const groups = Object.values(wallets).filter(wallet => wallet.type === WalletTypes.mnemonic); + + return ( + + + + + + 􀆉 + + + + + {i18n.t(i18n.l.wallet.new.choose_wallet_group.title)} + + + {i18n.t(i18n.l.wallet.new.choose_wallet_group.description)} + + + + + + + + {groups.length > 0 && ( + + {groups.map(wallet => ( + + ))} + + )} + + + ); +} diff --git a/src/navigation/Routes.android.tsx b/src/navigation/Routes.android.tsx index 1e03c2d96a3..3123532bbec 100644 --- a/src/navigation/Routes.android.tsx +++ b/src/navigation/Routes.android.tsx @@ -37,6 +37,7 @@ import { stackNavigationConfig, learnWebViewScreenConfig, backupSheetSizes, + panelConfig, } from './config'; import { addWalletNavigatorPreset, @@ -93,6 +94,7 @@ import { RootStackParamList } from './types'; import WalletLoadingListener from '@/components/WalletLoadingListener'; import { Portal as CMPortal } from '@/react-native-cool-modals/Portal'; import { NetworkSelector } from '@/components/NetworkSwitcher'; +import { ChooseWalletGroup } from './ChooseWalletGroup'; const Stack = createStackNavigator(); const OuterStack = createStackNavigator(); @@ -161,6 +163,7 @@ function BSNavigator() { + + {profilesEnabled && ( <> diff --git a/src/navigation/routesNames.ts b/src/navigation/routesNames.ts index ff4372906e3..3615eede1e2 100644 --- a/src/navigation/routesNames.ts +++ b/src/navigation/routesNames.ts @@ -4,6 +4,7 @@ const Routes = { ADD_CASH_SCREEN_NAVIGATOR: 'AddCashSheetNavigator', ADD_CASH_SHEET: 'AddCashSheet', ADD_WALLET_NAVIGATOR: 'AddWalletNavigator', + CHOOSE_WALLET_GROUP: 'ChooseWalletGroup', ADD_WALLET_SHEET: 'AddWalletSheet', APP_ICON_UNLOCK_SHEET: 'AppIconUnlockSheet', AVATAR_BUILDER: 'AvatarBuilder', diff --git a/src/screens/AddWalletSheet.tsx b/src/screens/AddWalletSheet.tsx index 92712d4241d..b0d4450cb22 100644 --- a/src/screens/AddWalletSheet.tsx +++ b/src/screens/AddWalletSheet.tsx @@ -3,28 +3,20 @@ import { AddWalletItem } from '@/components/add-wallet/AddWalletRow'; import { Box, globalColors, Inset } from '@/design-system'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; -import React, { useRef } from 'react'; +import React from 'react'; import * as i18n from '@/languages'; import { HARDWARE_WALLETS, useExperimentalFlag } from '@/config'; import { analytics, analyticsV2 } from '@/analytics'; import { InteractionManager } from 'react-native'; -import { createAccountForWallet, walletsLoadState } from '@/redux/wallets'; -import { createWallet } from '@/model/wallet'; -import WalletTypes from '@/helpers/walletTypes'; import { logger, RainbowError } from '@/logger'; import WalletsAndBackup from '@/assets/WalletsAndBackup.png'; import CreateNewWallet from '@/assets/CreateNewWallet.png'; import PairHairwareWallet from '@/assets/PairHardwareWallet.png'; import ImportSecretPhraseOrPrivateKey from '@/assets/ImportSecretPhraseOrPrivateKey.png'; import WatchWalletIcon from '@/assets/watchWallet.png'; -import { useDispatch } from 'react-redux'; -import showWalletErrorAlert from '@/helpers/support'; import { cloudPlatform } from '@/utils/platform'; import { RouteProp, useRoute } from '@react-navigation/native'; -import { useInitializeWallet, useWallets } from '@/hooks'; -import { WalletLoadingStates } from '@/helpers/walletLoadingStates'; import { executeFnIfCloudBackupAvailable } from '@/model/backup'; -import { walletLoadingStore } from '@/state/walletLoading/walletLoading'; const TRANSLATIONS = i18n.l.wallet.new.add_wallet_sheet; @@ -44,10 +36,6 @@ export const AddWalletSheet = () => { const { goBack, navigate } = useNavigation(); const hardwareWalletsEnabled = useExperimentalFlag(HARDWARE_WALLETS); - const dispatch = useDispatch(); - const initializeWallet = useInitializeWallet(); - const creatingWallet = useRef(); - const { isDamaged, selectedWallet, wallets } = useWallets(); const onPressCreate = async () => { try { @@ -56,97 +44,8 @@ export const AddWalletSheet = () => { type: 'new', }); analytics.track('Tapped "Create a new wallet"'); - if (creatingWallet.current) return; - creatingWallet.current = true; - // Show naming modal - InteractionManager.runAfterInteractions(() => { - goBack(); - }); - InteractionManager.runAfterInteractions(() => { - setTimeout(() => { - navigate(Routes.MODAL_SCREEN, { - actionType: 'Create', - asset: [], - isNewProfile: true, - onCancel: () => { - creatingWallet.current = false; - }, - onCloseModal: async (args: any) => { - if (args) { - walletLoadingStore.setState({ - loadingState: WalletLoadingStates.CREATING_WALLET, - }); - - const name = args?.name ?? ''; - const color = args?.color ?? null; - // Check if the selected wallet is the primary - let primaryWalletKey = selectedWallet.primary ? selectedWallet.id : null; - - // If it's not, then find it - !primaryWalletKey && - Object.keys(wallets as any).some(key => { - const wallet = wallets?.[key]; - if (wallet?.type === WalletTypes.mnemonic && wallet.primary) { - primaryWalletKey = key; - return true; - } - return false; - }); - - // If there's no primary wallet at all, - // we fallback to an imported one with a seed phrase - !primaryWalletKey && - Object.keys(wallets as any).some(key => { - const wallet = wallets?.[key]; - if (wallet?.type === WalletTypes.mnemonic && wallet.imported) { - primaryWalletKey = key; - return true; - } - return false; - }); - try { - // If we found it and it's not damaged use it to create the new account - if (primaryWalletKey && !wallets?.[primaryWalletKey].damaged) { - await dispatch(createAccountForWallet(primaryWalletKey, color, name)); - // @ts-ignore - await initializeWallet(); - } else { - // If doesn't exist, we need to create a new wallet - await createWallet({ - color, - name, - clearCallbackOnStartCreation: true, - }); - await dispatch(walletsLoadState()); - // @ts-expect-error - needs refactor to object params - await initializeWallet(); - } - } catch (e) { - logger.error(new RainbowError('[AddWalletSheet]: Error while trying to add account'), { - error: e, - }); - if (isDamaged) { - setTimeout(() => { - showWalletErrorAlert(); - }, 1000); - } - } finally { - walletLoadingStore.setState({ - loadingState: null, - }); - } - } - creatingWallet.current = false; - }, - profile: { - color: null, - name: ``, - }, - type: 'wallet_profile', - }); - }, 50); - }); + navigate(Routes.CHOOSE_WALLET_GROUP, {}); } catch (e) { logger.error(new RainbowError('[AddWalletSheet]: Error while trying to add account'), { error: e, diff --git a/src/utils/abbreviations.ts b/src/utils/abbreviations.ts index 4b595f4714a..663043e530d 100644 --- a/src/utils/abbreviations.ts +++ b/src/utils/abbreviations.ts @@ -16,7 +16,7 @@ export function formatAddressForDisplay(text: string, truncationLength = 4, firs return isValidDomainFormat(text) ? text : address(text, truncationLength, firstSectionLength); } -export function abbreviateEnsForDisplay(text: string, truncationLength = 20, truncationLengthBuffer = 2): string | null { +export function abbreviateEnsForDisplay(text: string | undefined, truncationLength = 20, truncationLengthBuffer = 2): string | undefined { if (typeof text !== 'string' || !isValidDomainFormat(text)) { return text; }