diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 22b46b2b2..de4f044c8 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -30,6 +30,7 @@ import { useGlobalAppPreventScreenrecordOnDev } from './hooks/appSettings'; import { useAppPreventScreenshotOnScreen } from './hooks/navigation'; import { useAutoGoogleSignIfPreviousSignedOnTop } from './hooks/cloudStorage'; import { useNoLongerSupports } from './components2024/NoLongerSupports/useNoLongerSupports'; +import { useCurrentAccountOnAppTop } from './hooks/account'; const rneuiTheme = createTheme({ lightColors: { @@ -56,6 +57,7 @@ function MainScreen({ rabbitCode }: AppProps) { useAppPreventScreenshotOnScreen(); useAutoGoogleSignIfPreviousSignedOnTop(); useNoLongerSupports(); + useCurrentAccountOnAppTop(); const initAccounts = useMemoizedFn(async () => { const accounts = await keyringService.getAllVisibleAccountsArray(); diff --git a/apps/mobile/src/AppNavigation.tsx b/apps/mobile/src/AppNavigation.tsx index af8d4569b..d6bc7f346 100644 --- a/apps/mobile/src/AppNavigation.tsx +++ b/apps/mobile/src/AppNavigation.tsx @@ -56,7 +56,7 @@ import { ScannerScreen } from './screens/Scanner/ScannerScreen'; import { FloatViewAutoLockCount } from './screens/Settings/components/FloatView'; import UnlockScreen from './screens/Unlock/Unlock'; import { SingleAddressNavigator } from './screens/Navigators/SingleAddressNavigator'; -import { GlobalAccountSwitcherStub } from './components/AccountSwitcher/SheetModal'; +// import { GlobalAccountSwitcherStub } from './components/AccountSwitcher/SheetModal'; const RootStack = createNativeStackNavigator(); diff --git a/apps/mobile/src/components/AccountSwitcher/AccountsPanel.tsx b/apps/mobile/src/components/AccountSwitcher/AccountsPanel.tsx index 0c83bc8a2..82a899e32 100644 --- a/apps/mobile/src/components/AccountSwitcher/AccountsPanel.tsx +++ b/apps/mobile/src/components/AccountSwitcher/AccountsPanel.tsx @@ -10,12 +10,18 @@ import { import { default as RcCaretDownCC } from './icons/caret-down-cc.svg'; import TouchableView from '../Touchable/TouchableView'; -import { useSceneAccountInfo } from '@/hooks/accountsSwitcher'; -import { AccountSwitcherAopProps } from './hooks'; +import { + isSameAccount, + useSceneAccountInfo, + useSwitchSceneCurrentAccount, +} from '@/hooks/accountsSwitcher'; +import { AccountSwitcherAopProps, useAccountSceneVisible } from './hooks'; import React, { useCallback, useEffect, useMemo } from 'react'; import { AddressItem } from '@/components2024/AddressItem/AddressItem'; import { ICONS_COMMON_2024 } from '@/assets2024/icons/common'; import RcIconCorrectCC from './icons/correct-cc.svg'; +import { apisAccountSwitch } from '@/core/apis'; +import { Account } from '@/core/services/preference'; const MY_ADDRESS_LIMIT = 3; @@ -35,12 +41,12 @@ function AddressItemInPanel({ addressItemProps, isCurrent, isPinned, - onPressAddress, + onPressAddress: proponPressAddress, }: { - addressItemProps: AddressItemProps; + addressItemProps: AddressItemProps & { account: Account }; isCurrent?: boolean; isPinned?: boolean; - onPressAddress?: () => void; + onPressAddress?: (account: Account) => void; } & RNViewProps) { const { styles, colors2024 } = useTheme2024({ getStyle: getAddressItemInPanelStyle, @@ -48,6 +54,11 @@ function AddressItemInPanel({ const [isPressing, setIsPressing] = React.useState(false); + const { account } = addressItemProps; + const onPressAddress = useCallback(() => { + proponPressAddress?.(account); + }, [account, proponPressAddress]); + return ( ) { - const { styles, colors2024 } = useTheme2024({ getStyle: getPanelStyle }); + const { styles } = useTheme2024({ getStyle: getPanelStyle }); + + const { isVisible, toggleSceneVisible } = useAccountSceneVisible(forScene); const { isPinnedAccount, - sceneCurrentAccount, + finalSceneCurrentAccount, myAddresses, safeAddresses, watchAddresses, } = useSceneAccountInfo({ forScene, - disableAutoFetch: false, + // disableAutoFetch: false, }); + const { switchSceneCurrentAccount } = useSwitchSceneCurrentAccount(); + const scrollViewRef = React.useRef(null); const scrollToBottom = useCallback(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); @@ -264,6 +279,16 @@ export function AccountsPanelInModal({ const [watchAddressNavCollapsed, setWatchAddressNavCollapsed] = React.useState(true); + const handlePressAccount = useCallback< + React.ComponentProps['onPressAddress'] & object + >( + async account => { + switchSceneCurrentAccount(forScene, account); + toggleSceneVisible(forScene, false); + }, + [forScene, switchSceneCurrentAccount, toggleSceneVisible], + ); + return ( @@ -276,10 +301,10 @@ export function AccountsPanelInModal({ {myAddresses.map((account, index) => { const key = `account-${account.address}-${account.brandName}-${index}`; - const isCurrent = - sceneCurrentAccount?.address === account.address && - sceneCurrentAccount?.brandName === account.brandName && - sceneCurrentAccount?.type === account.type; + const isCurrent = isSameAccount( + account, + finalSceneCurrentAccount, + ); return ( 0 && styles.addressItemTopGap]} /> ); @@ -307,16 +333,18 @@ export function AccountsPanelInModal({ {safeAddresses.map((account, index) => { const key = `account-${account.address}-${account.brandName}-${index}`; - const isCurrent = - sceneCurrentAccount?.address === account.address && - sceneCurrentAccount?.brandName === account.brandName && - sceneCurrentAccount?.type === account.type; + const isCurrent = isSameAccount( + account, + finalSceneCurrentAccount, + ); return ( 0 && styles.addressItemTopGap]} /> ); @@ -339,16 +367,18 @@ export function AccountsPanelInModal({ {watchAddresses.map((account, index) => { const key = `account-${account.address}-${account.brandName}-${index}`; - const isCurrent = - sceneCurrentAccount?.address === account.address && - sceneCurrentAccount?.brandName === account.brandName && - sceneCurrentAccount?.type === account.type; + const isCurrent = isSameAccount( + account, + finalSceneCurrentAccount, + ); return ( 0 && styles.addressItemTopGap]} /> ); diff --git a/apps/mobile/src/components/AccountSwitcher/Modal.tsx b/apps/mobile/src/components/AccountSwitcher/Modal.tsx index a2c461a06..f2cadec59 100644 --- a/apps/mobile/src/components/AccountSwitcher/Modal.tsx +++ b/apps/mobile/src/components/AccountSwitcher/Modal.tsx @@ -1,6 +1,10 @@ -import { Dimensions, Text, TouchableOpacity, View } from 'react-native'; -import { AccountSwitcherAopProps, useAccountSwitcherScenes } from './hooks'; -import { createGetStyles2024, makeDevOnlyStyle } from '@/utils/styles'; +import { TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native'; +import { AccountSwitcherAopProps, useAccountSceneVisible } from './hooks'; +import { + createGetStyles2024, + makeDebugBorder, + makeDevOnlyStyle, +} from '@/utils/styles'; import { useTheme2024 } from '@/hooks/theme'; import { useSafeOffTop } from '@/hooks/useAppLayout'; import { ScreenWithAccountSwitcherLayouts } from '@/constant/layout'; @@ -14,7 +18,7 @@ export function AccountSwitcherModal({ }: AccountSwitcherAopProps<{ inScreen?: boolean; }>) { - const { isVisible, toggleSceneVisible } = useAccountSwitcherScenes(forScene); + const { isVisible, toggleSceneVisible } = useAccountSceneVisible(forScene); const { styles } = useTheme2024({ getStyle: getModalStyle }); @@ -36,7 +40,7 @@ export function AccountSwitcherModal({ }; return ( - { - setTimeout(() => { - toggleSceneVisible(forScene, false); - }, 50); + onPressIn={() => { + toggleSceneVisible(forScene, false); }} style={[styles.bgMask, { height: absoluteStyle.maxHeight }]} + delayLongPress={1000} /> - - - - + { + // toggleSceneVisible(forScene, false); + // }} + > + + + ); } @@ -77,7 +84,8 @@ const getModalStyle = createGetStyles2024(ctx => { panelContainer: { position: 'relative', width: '100%', - maxHeight: '80%', + // height: '50%', + maxHeight: '90%', // ...makeDevOnlyStyle({ // backgroundColor: 'blue', // }), diff --git a/apps/mobile/src/components/AccountSwitcher/OnScreenHeader.tsx b/apps/mobile/src/components/AccountSwitcher/OnScreenHeader.tsx index ef44fa237..c829f464b 100644 --- a/apps/mobile/src/components/AccountSwitcher/OnScreenHeader.tsx +++ b/apps/mobile/src/components/AccountSwitcher/OnScreenHeader.tsx @@ -1,7 +1,7 @@ import { FontNames } from '@/core/utils/fonts'; import { useTheme2024 } from '@/hooks/theme'; import { createGetStyles2024, makeDebugBorder } from '@/utils/styles'; -import { useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { Text, View } from 'react-native'; // caret-down-cc.svg @@ -10,10 +10,11 @@ import TouchableView from '../Touchable/TouchableView'; import { AccountSwitcherAopProps, AccountSwitcherScene, - useAccountSwitcherScenes, + useAccountSceneVisible, } from './hooks'; import { - useSceneCurrentAccount, + useSceneAccountInfo, + useSwitchSceneCurrentAccount, useSwitchAccountBeforeEnterScene, } from '@/hooks/accountsSwitcher'; import { ellipsisAddress } from '@/utils/address'; @@ -21,15 +22,21 @@ import { ellipsisAddress } from '@/utils/address'; export function ScreenHeaderAccountSwitcher({ titleText = '', forScene, + showType = 'account', }: RNViewProps & AccountSwitcherAopProps<{ titleText?: React.ReactNode; + showType?: 'addresses' | 'account'; }>) { const { colors2024, styles } = useTheme2024({ getStyle }); const { isVisible: isOpen, toggleSceneVisible } = - useAccountSwitcherScenes(forScene); - const { sceneCurrentAccount } = useSceneCurrentAccount(forScene); + useAccountSceneVisible(forScene); + const { switchSceneCurrentAccount } = useSwitchSceneCurrentAccount(); + const { finalSceneCurrentAccount, totalCountOfAccount, myAddresses } = + useSceneAccountInfo({ + forScene, + }); const { preFetchData } = useSwitchAccountBeforeEnterScene(); const titleTextNode = useMemo(() => { @@ -40,27 +47,113 @@ export function ScreenHeaderAccountSwitcher({ ); }, [titleText, styles]); - // const isOpen = getSceneVisible(forScene); + useEffect(() => { + switchSceneCurrentAccount(forScene, finalSceneCurrentAccount); + }, [finalSceneCurrentAccount, forScene, switchSceneCurrentAccount]); - if (!sceneCurrentAccount?.address) { + const needShowPicker = totalCountOfAccount > 1 && !!myAddresses.length; + if (showType === 'account' && !finalSceneCurrentAccount?.address) { + return titleTextNode; + } else if (showType === 'addresses' && !needShowPicker) { + return titleTextNode; + } + + return ( + { + const nextOpen = !isOpen; + toggleSceneVisible(forScene, nextOpen); + if (nextOpen) { + preFetchData(); + } + }}> + {titleTextNode} + {showType === 'account' && ( + <> + {!!finalSceneCurrentAccount?.address && ( + + + {ellipsisAddress(finalSceneCurrentAccount?.address)} + + + + + )} + + )} + {showType === 'addresses' && ( + <> + {needShowPicker && ( + + + {Math.max(myAddresses.length, 0)} addresses + + + + + )} + + )} + + ); +} + +/** @deprecated */ +export function ScreenHeaderAccountPicker({ + titleText = '', + forScene, +}: RNViewProps & + AccountSwitcherAopProps<{ + titleText?: React.ReactNode; + }>) { + const { colors2024, styles } = useTheme2024({ getStyle }); + + const { isVisible: isOpen, toggleSceneVisible } = + useAccountSceneVisible(forScene); + const { totalCountOfAccount, myAddresses } = useSceneAccountInfo({ + forScene, + }); + const { preFetchData } = useSwitchAccountBeforeEnterScene(); + + const titleTextNode = useMemo(() => { + return typeof titleText === 'string' ? ( + {titleText} + ) : ( + titleText + ); + }, [titleText, styles]); + + const needShowPicker = totalCountOfAccount > 1 && !!myAddresses.length; + if (!needShowPicker) { return titleTextNode; } return ( - + { + const nextOpen = !isOpen; + toggleSceneVisible(forScene, nextOpen); + if (nextOpen) { + preFetchData(); + } + }}> {titleTextNode} - {!!sceneCurrentAccount?.address && ( - { - const nextOpen = !isOpen; - toggleSceneVisible(forScene, nextOpen); - if (nextOpen) { - preFetchData(); - } - }}> + {needShowPicker && ( + - {ellipsisAddress(sceneCurrentAccount?.address)} + {Math.max(myAddresses.length, 0)} addresses - + )} - + ); } diff --git a/apps/mobile/src/components/AccountSwitcher/SheetModal.tsx b/apps/mobile/src/components/AccountSwitcher/SheetModal.tsx index 00e4a2471..deb459d3e 100644 --- a/apps/mobile/src/components/AccountSwitcher/SheetModal.tsx +++ b/apps/mobile/src/components/AccountSwitcher/SheetModal.tsx @@ -12,7 +12,7 @@ import AutoLockView from '../AutoLockView'; import { AccountSwitcherAopProps, AccountSwitcherScene, - useAccountSwitcherScenes, + useAccountSceneVisible, } from './hooks'; import { RefreshAutoLockBottomSheetBackdrop } from '../patches/refreshAutoLockUI'; import useMount from 'react-use/lib/useMount'; @@ -29,7 +29,7 @@ const renderBackdrop = (props: BottomSheetBackdropProps) => ( export function AccountsSwitcherSheetModal({ forScene, }: AccountSwitcherAopProps) { - const { isVisible } = useAccountSwitcherScenes(forScene); + const { isVisible } = useAccountSceneVisible(forScene); const sheetModalRef = useRef(null); const { styles } = useTheme2024({ getStyle: getModalStyle }); diff --git a/apps/mobile/src/components/AccountSwitcher/hooks.ts b/apps/mobile/src/components/AccountSwitcher/hooks.ts index 7eebba98a..247b8da7d 100644 --- a/apps/mobile/src/components/AccountSwitcher/hooks.ts +++ b/apps/mobile/src/components/AccountSwitcher/hooks.ts @@ -21,15 +21,21 @@ function makeDefaultState(): AccountSwitcherState { collapsed: true, }; } -const DefaultStates = { +const DefaultStates: { + [key in AccountSwitcherScene]: AccountSwitcherState; +} = { Send: makeDefaultState(), + SendNFT: makeDefaultState(), Swap: makeDefaultState(), Bridge: makeDefaultState(), + + History: makeDefaultState(), + // HistoryFilterScam: makeDefaultState(), }; export const screenHeaderAccountSwitcherAtom = atom(DefaultStates); -export function useAccountSwitcherScenes(forScene?: AccountSwitcherScene) { +export function useAccountSceneVisible(forScene?: AccountSwitcherScene) { const [scenes, setScenes] = useAtom(screenHeaderAccountSwitcherAtom); const toggleSceneVisible = useCallback( @@ -51,7 +57,7 @@ export function useAccountSwitcherScenes(forScene?: AccountSwitcherScene) { (scene: AccountSwitcherScene) => { if (__DEV__ && !scenes.hasOwnProperty(scene)) { console.error( - `[useAccountSwitcherScenes] AccountSwitcher scene "${scene}" not found in state`, + `[useAccountSceneVisible] AccountSwitcher scene "${scene}" not found in state`, ); } diff --git a/apps/mobile/src/constant/layout.ts b/apps/mobile/src/constant/layout.ts index 77f00a600..d2da8423d 100644 --- a/apps/mobile/src/constant/layout.ts +++ b/apps/mobile/src/constant/layout.ts @@ -1,5 +1,5 @@ import { NativeStackNavigationOptions } from '@react-navigation/native-stack'; -import { AppColorsVariants, ThemeColors } from './theme'; +import { AppColorsVariants, ThemeColors, ThemeColors2024 } from './theme'; import { IS_ANDROID } from '@/core/native/utils'; export const ModalLayouts = { @@ -171,6 +171,7 @@ function makeScreenSpecConfig() { : ('dark' as const); const colors = ThemeColors[isDarkTheme ? 'dark' : 'light']; + const colors2024 = ThemeColors2024[isDarkTheme ? 'dark' : 'light']; const bg1DefaultConf = { barStyle: adaptiveStatusBarStyle, @@ -178,6 +179,12 @@ function makeScreenSpecConfig() { androidStatusBarBg: colors['neutral-bg1'], }; + const bg1Default2024Conf = { + barStyle: adaptiveStatusBarStyle, + iosStatusBarStyle: adaptiveIosStatusBarStyle, + androidStatusBarBg: colors2024['neutral-bg1'], + }; + const bg2DefaultConf = { barStyle: adaptiveStatusBarStyle, iosStatusBarStyle: adaptiveIosStatusBarStyle, @@ -215,6 +222,7 @@ function makeScreenSpecConfig() { SampleNewUserGetStarted2024: bg1DefaultConf, Home: bg1DefaultConf, + MultiAddressHome: bg1Default2024Conf, Unlock: bg1DefaultConf, // Dapps: !isDarkTheme ? card2DefaultConf : bg1DefaultConf, diff --git a/apps/mobile/src/core/apis/accountSwitch.ts b/apps/mobile/src/core/apis/accountSwitch.ts new file mode 100644 index 000000000..d3ee6546e --- /dev/null +++ b/apps/mobile/src/core/apis/accountSwitch.ts @@ -0,0 +1,26 @@ +import { preferenceService } from '../services'; +import { Account } from '../services/preference'; + +export function setLastUsedAccount(account: Account) { + preferenceService.setLastUsedAccount(account); +} + +export async function getLastUsedAccount() { + return preferenceService.getLastUsedAccount(); +} + +export async function enableSceneAccount(account?: Account) { + if (account) { + preferenceService.setLastUsedAccount(account); + } + + await preferenceService.activateLastUsedAccount(); + + const dispose = () => { + return preferenceService.inactivateLastUsedAccount(); + }; + + return { + dispose, + }; +} diff --git a/apps/mobile/src/core/apis/index.ts b/apps/mobile/src/core/apis/index.ts index 4a53d2210..fb23d1196 100644 --- a/apps/mobile/src/core/apis/index.ts +++ b/apps/mobile/src/core/apis/index.ts @@ -20,3 +20,4 @@ export * as apiMnemonic from './mnemonic'; export * as apiToken from './token'; export { apiCustomTestnet } from './customTestnet'; export { apiCustomRPC } from './customRPC'; +export * as apisAccountSwitch from './accountSwitch'; diff --git a/apps/mobile/src/core/apis/serviceEvent.ts b/apps/mobile/src/core/apis/serviceEvent.ts index 8d01f5e39..f4cfddfff 100644 --- a/apps/mobile/src/core/apis/serviceEvent.ts +++ b/apps/mobile/src/core/apis/serviceEvent.ts @@ -1,7 +1,7 @@ import { BroadcastEvent } from '@/constant/event'; import { ServiceEvent } from '@rabby-wallet/biz-utils'; -export const enum AppServiceEvent { +const enum AppServiceEvent { foo = 'foo', } diff --git a/apps/mobile/src/core/services/_utils.ts b/apps/mobile/src/core/services/_utils.ts new file mode 100644 index 000000000..ae06c2584 --- /dev/null +++ b/apps/mobile/src/core/services/_utils.ts @@ -0,0 +1,26 @@ +import EventEmitter from 'events'; +import type { Account } from './preference'; + +type Listener = (resp?: any) => void; + +function makeJsEEClass>() { + type EE = typeof EventEmitter & { + on( + eventType: T, + listener: Listeners[T], + context?: Object, + ): void; + emit( + eventType: T, + ...args: Parameters + ): void; + }; + + return { EventEmitter: EventEmitter as EE }; +} + +const { EventEmitter: AppServiceEvents } = makeJsEEClass<{ + currentAccountChanged: (account: Account) => void; +}>(); + +export const appServiceEvents = new AppServiceEvents(); diff --git a/apps/mobile/src/core/services/dappService.ts b/apps/mobile/src/core/services/dappService.ts index 288ae4335..01ca22838 100644 --- a/apps/mobile/src/core/services/dappService.ts +++ b/apps/mobile/src/core/services/dappService.ts @@ -3,6 +3,7 @@ import type { StorageAdapaterOptions } from '@rabby-wallet/persist-store'; import { StoreServiceBase } from '@rabby-wallet/persist-store'; import type { BasicDappInfo } from '@rabby-wallet/rabby-api/dist/types'; import { INTERNAL_REQUEST_ORIGIN } from '@/constant'; +import { Account } from './preference'; export interface DappInfo { origin: string; @@ -14,6 +15,7 @@ export interface DappInfo { chainId: CHAINS_ENUM; lastPath?: string; // 待定 lastPathTimeAt?: number; // + currentAccount?: Account; } export type DappStore = { diff --git a/apps/mobile/src/core/services/preference.ts b/apps/mobile/src/core/services/preference.ts index 4d187c7cb..4eda9670c 100644 --- a/apps/mobile/src/core/services/preference.ts +++ b/apps/mobile/src/core/services/preference.ts @@ -14,6 +14,7 @@ import { KeyringAccountWithAlias } from '@/hooks/account'; import { BroadcastEvent } from '@/constant/event'; import KeyringService from '@rabby-wallet/service-keyring'; import { DEFAULT_AUTO_LOCK_MINUTES } from '@/constant/autoLock'; +import { appServiceEvents } from './_utils'; const { isSameAddress } = addressUtils; @@ -88,6 +89,16 @@ export interface PreferenceStore { * The unique visitor ID */ extensionId?: string; + + /** + * For Send, Swap, Bridge, etc, default is first account in the account list + */ + lastUsedAccount?: Account; + + /** + * For temporary account switch + */ + tempCurrentAccount?: Account; } export interface AddressSortStore { @@ -106,6 +117,7 @@ export class PreferenceService { store!: PreferenceStore; keyringService: KeyringService; sessionService: import('./session').SessionService; + // globalSerivceEvents: typeof import('../apis/serviceEvent').globalSerivceEvents; constructor( options: StorageAdapaterOptions & { @@ -144,12 +156,19 @@ export class PreferenceService { ...defaultAddressSortStore, }, isInvited: false, + lastUsedAccount: undefined, + tempCurrentAccount: undefined, }, }, { storage: options?.storageAdapter, }, ); + + // reset current account if app not closed properly + if (this.store.tempCurrentAccount) { + this.store.currentAccount = this.store.tempCurrentAccount; + } } /* eslint-disable no-dupe-class-members */ @@ -254,7 +273,44 @@ export class PreferenceService { this.sessionService.broadcastEvent(BroadcastEvent.accountsChanged, [ account.address.toLowerCase(), ]); - // syncStateToUI(BROADCAST_TO_UI_EVENTS.accountsChanged, account); + appServiceEvents.emit('currentAccountChanged', account); + } + }; + + getLastUsedAccount = async (): Promise => { + const account = cloneDeep(this.store.lastUsedAccount); + if (account) { + return account; + } + // TODO: 排序 + // return the first account in the account list + const [first] = await this.keyringService.getAllVisibleAccountsArray(); + + return first; + }; + + setLastUsedAccount = (account: Account) => { + this.store.lastUsedAccount = account; + }; + + activateLastUsedAccount = async () => { + const prevAccount = this.getCurrentAccount(); + + if (prevAccount) { + this.store.tempCurrentAccount = prevAccount; + } + + const account = await this.getLastUsedAccount(); + // console.debug('[LastUsedAccount] activate', account); + this.setCurrentAccount(account); + }; + + inactivateLastUsedAccount = () => { + const tempAccount = this.store.tempCurrentAccount; + + // console.debug('[LastUsedAccount] restore', tempAccount); + if (tempAccount) { + this.setCurrentAccount(tempAccount); } }; diff --git a/apps/mobile/src/hooks/account.ts b/apps/mobile/src/hooks/account.ts index 709e282b0..47dc4cdb1 100644 --- a/apps/mobile/src/hooks/account.ts +++ b/apps/mobile/src/hooks/account.ts @@ -25,6 +25,7 @@ import { coerceFloat } from '@/utils/number'; import { requestOpenApiMultipleNets } from '@/utils/openapi'; import { apiBalance } from '@/core/apis'; import { useAtomicRequest } from './common/useAtomicAction'; +import { appServiceEvents } from '@/core/services/_utils'; export type KeyringAccountWithAlias = KeyringAccount & { aliasName?: string; @@ -114,7 +115,10 @@ const fetchingCurrentAccountAtom = atom(false); * @description this hooks GET CURRENT account and re-PICK it from accounts, so you need to * ensure the accounts is fetched/updated before using this hook */ -export function useCurrentAccount(options?: { disableAutoFetch?: boolean }) { +export function useCurrentAccount(options?: { + disableAutoFetch?: boolean; + isTop?: true; +}) { const [currentAccount, setCurrentAccount] = useAtom(currentAccountAtom); const [accounts] = useAtom(accountsAtom); @@ -152,7 +156,7 @@ export function useCurrentAccount(options?: { disableAutoFetch?: boolean }) { [setCurrentAccount], ); - const { disableAutoFetch = false } = options || {}; + const { disableAutoFetch = false, isTop = false } = options || {}; useEffect(() => { if (!disableAutoFetch) { @@ -168,6 +172,23 @@ export function useCurrentAccount(options?: { disableAutoFetch?: boolean }) { }; } +/** + * @description this hooks will listen to the event of current account changed + */ +export function useCurrentAccountOnAppTop() { + const { fetchCurrentAccountAsync } = useCurrentAccount(); + useEffect(() => { + const listener = () => { + fetchCurrentAccountAsync(); + }; + appServiceEvents.on('currentAccountChanged', listener); + + return () => { + appServiceEvents.off('currentAccountChanged', listener); + }; + }, [fetchCurrentAccountAsync]); +} + export const usePinAddresses = (opts?: { disableAutoFetch?: boolean }) => { const { disableAutoFetch = false } = opts || {}; const [pinAddresses, setPinAddresses] = useAtom(pinAddressesAtom); diff --git a/apps/mobile/src/hooks/accountsSwitcher.ts b/apps/mobile/src/hooks/accountsSwitcher.ts index 2bd80230a..30b4752eb 100644 --- a/apps/mobile/src/hooks/accountsSwitcher.ts +++ b/apps/mobile/src/hooks/accountsSwitcher.ts @@ -3,24 +3,28 @@ import { atomByMMKV } from '@/core/storage/mmkv'; import type { Account, IPinAddress } from '@/core/services/preference'; import { useAccounts, useCurrentAccount, usePinAddresses } from './account'; import { useCallback, useEffect, useMemo } from 'react'; -import { useAtom } from 'jotai'; +import { atom, useAtom } from 'jotai'; import { useSortAddressList } from '@/screens/Address/useSortAddressList'; import { KEYRING_CLASS } from '@rabby-wallet/keyring-utils'; +import { apisAccountSwitch } from '@/core/apis'; const AccountSwitcherInfos = { Send: makeSceneAccount(), + SendNFT: makeSceneAccount(), Swap: makeSceneAccount(), Bridge: makeSceneAccount(), + + History: makeSceneAccount(), + // HistoryFilterScam: makeSceneAccount(), // treat HistoryFilterScam screen as History screen }; export type AccountSwitcherScene = keyof typeof AccountSwitcherInfos; -type SceneAccounts = Record< - AccountSwitcherScene, - { +type SceneAccounts = { + [K in AccountSwitcherScene]?: { currentAccount: Account | null; - } ->; + }; +}; function makeSceneAccount() { return { @@ -33,14 +37,14 @@ export const sceneAccountInfoAtom = atomByMMKV( ); export function useSwitchAccountBeforeEnterScene() { - const [sceneAccountInfo, setSceneAccountInfo] = useAtom(sceneAccountInfoAtom); + const [, setSceneAccountInfo] = useAtom(sceneAccountInfoAtom); - const { accounts, fetchAccounts } = useAccounts({ disableAutoFetch: true }); + const { fetchAccounts } = useAccounts({ disableAutoFetch: true }); const { fetchCurrentAccountAsync } = useCurrentAccount({ disableAutoFetch: true, }); - const { pinAddresses, getPinAddressesAsync } = usePinAddresses({ + const { getPinAddressesAsync } = usePinAddresses({ disableAutoFetch: true, }); @@ -73,54 +77,88 @@ export function useSwitchAccountBeforeEnterScene() { }; } -export function useSceneCurrentAccount(forScene: AccountSwitcherScene) { - const [sceneAccountInfo, setSceneAccountInfo] = useAtom(sceneAccountInfoAtom); +const lastUsedAccountAtom = atom(null); +export function useLastUsedAccount(options?: { disableAutoFetch?: boolean }) { + const [lastUsedAccount, setLastUsedAccount] = useAtom(lastUsedAccountAtom); - // const { accounts, fetchAccounts } = useAccounts({ disableAutoFetch: true }); - const { currentAccount, fetchCurrentAccount } = useCurrentAccount({ - disableAutoFetch: true, - }); + const { disableAutoFetch } = options || {}; - const setSceneCurrentAccount = useCallback( - (scene: AccountSwitcherScene, account: Account | null) => { - setSceneAccountInfo(prev => ({ - ...prev, - [scene]: { - ...prev[scene], - currentAccount: account, - }, - })); + const fetchLastUsedAccount = useCallback(async () => { + const lastUsedAccount = await apisAccountSwitch.getLastUsedAccount(); + setLastUsedAccount(lastUsedAccount); + }, [setLastUsedAccount]); + + useEffect(() => { + if (!disableAutoFetch) { + fetchLastUsedAccount(); + } + }, [disableAutoFetch, fetchLastUsedAccount]); + + return { + lastUsedAccount, + fetchLastUsedAccount, + }; +} + +export function useSwitchSceneCurrentAccount() { + const [, setSceneAccountInfo] = useAtom(sceneAccountInfoAtom); + + const switchSceneCurrentAccount = useCallback( + async (scene: AccountSwitcherScene, account: Account | null) => { + setSceneAccountInfo(prev => { + if (account) { + apisAccountSwitch.enableSceneAccount(account); + + // avoid duplicate set same account + if (isSameAccount(account, prev[scene]?.currentAccount)) return prev; + } else if (!prev[scene]?.currentAccount) { + return prev; + } + + return { + ...prev, + [scene]: { + ...prev[scene], + currentAccount: account, + }, + }; + }); }, [setSceneAccountInfo], ); - const fetchSceneCurrentAccount = useCallback(async () => { - await Promise.allSettled([fetchCurrentAccount()]); - }, [fetchCurrentAccount]); - return { - // sceneCurrentAccount: sceneAccountInfo[forScene]?.currentAccount, - sceneCurrentAccount: currentAccount, - setSceneCurrentAccount, - fetchSceneCurrentAccount, + switchSceneCurrentAccount, }; } +export function isSameAccount( + account: Account, + saccount?: SceneAccount | null, +) { + if (!saccount) return false; + + return ( + saccount?.address === account.address && + saccount?.brandName === account.brandName && + saccount?.type === account.type + ); +} + type SceneAccount = Account & { isPinned?: boolean; }; export function useSceneAccountInfo(options: { forScene: AccountSwitcherScene; - disableAutoFetch?: boolean; }) { - const { accounts, fetchAccounts } = useAccounts({ disableAutoFetch: true }); + const { accounts } = useAccounts({ disableAutoFetch: true }); - // const { fetchCurrentAccount } = useCurrentAccount({ disableAutoFetch: true }); + const { forScene } = options || {}; + const [sceneAccountInfo] = useAtom(sceneAccountInfoAtom); - const { forScene, disableAutoFetch } = options || {}; - const { sceneCurrentAccount } = useSceneCurrentAccount(forScene); + const sceneCurrentAccount = sceneAccountInfo[forScene]?.currentAccount; - const { pinAddresses, getPinAddressesAsync } = usePinAddresses({ + const { pinAddresses } = usePinAddresses({ disableAutoFetch: true, }); @@ -141,10 +179,11 @@ export function useSceneAccountInfo(options: { const computed = useMemo(() => { const result = { - sceneCurrentAccountIndexInMyAddresses: -1, - sceneCurrentAccount: null as null | SceneAccount, + totalCountOfAccount: accounts.length, + // sceneCurrentAccountIndexInMyAddresses: -1, + finalSceneCurrentAccount: null as null | SceneAccount, myAddresses: [] as SceneAccount[], - myRestAddresses: [] as SceneAccount[], + // myRestAddresses: [] as SceneAccount[], watchAddresses: [] as SceneAccount[], safeAddresses: [] as SceneAccount[], }; @@ -152,30 +191,24 @@ export function useSceneAccountInfo(options: { for (const [idx, origAccount] of accounts.entries()) { const account: SceneAccount = { ...origAccount }; - // const mapKey = account.brandName + '-' + account.address; - // if (pinAddressesDict[mapKey]) { - // account.isPinned = true; - // } - if (account.type === KEYRING_CLASS.WATCH) { result.watchAddresses.push(account); } else if (account.type === KEYRING_CLASS.GNOSIS) { result.safeAddresses.push(account); } else { result.myAddresses.push(account); + } - if ( - sceneCurrentAccount && - account.address === sceneCurrentAccount?.address - ) { - result.sceneCurrentAccountIndexInMyAddresses = idx; - result.sceneCurrentAccount = account; - } else { - result.myRestAddresses.push(account); - } + if (isSameAccount(account, sceneCurrentAccount)) { + // result.sceneCurrentAccountIndexInMyAddresses = idx; + result.finalSceneCurrentAccount = sceneCurrentAccount!; } } + if (!result.finalSceneCurrentAccount && accounts.length) { + result.finalSceneCurrentAccount = accounts[0]; + } + return result; }, [accounts, sceneCurrentAccount]); diff --git a/apps/mobile/src/hooks/useDappLastUsedAccount.ts b/apps/mobile/src/hooks/useDappLastUsedAccount.ts new file mode 100644 index 000000000..e8d3a02a9 --- /dev/null +++ b/apps/mobile/src/hooks/useDappLastUsedAccount.ts @@ -0,0 +1,53 @@ +import { dappService, preferenceService } from '@/core/services'; +import { DappInfo } from '@/core/services/dappService'; +import React from 'react'; +import { useCurrentAccount } from './account'; + +/** + * auto activate and inactivate last used account in dapp + */ +export const useDappLastUsedAccount = () => { + const { switchAccount } = useCurrentAccount(); + + const doSwitchAccount = React.useCallback(() => { + const account = preferenceService.getCurrentAccount(); + if (account) { + switchAccount(account); + } + }, [switchAccount]); + + const activate = React.useCallback( + (dapp: DappInfo) => { + const prevAccount = preferenceService.getCurrentAccount(); + + if (prevAccount) { + preferenceService.store.tempCurrentAccount = prevAccount; + } + + dapp.currentAccount && switchAccount(dapp.currentAccount); + }, + [switchAccount], + ); + + const inactivate = React.useCallback(() => { + preferenceService.inactivateLastUsedAccount(); + doSwitchAccount(); + }, [doSwitchAccount]); + + const updateAccount = React.useCallback( + (dapp: DappInfo) => { + dappService.updateDapp(dapp); + + if (dapp.currentAccount) { + switchAccount(dapp.currentAccount); + } + }, + [switchAccount], + ); + + return { + activate, + inactivate, + updateAccount, + }; +}; diff --git a/apps/mobile/src/hooks/useLastUsedAccountInScreen.ts b/apps/mobile/src/hooks/useLastUsedAccountInScreen.ts new file mode 100644 index 000000000..48b2cbd20 --- /dev/null +++ b/apps/mobile/src/hooks/useLastUsedAccountInScreen.ts @@ -0,0 +1,47 @@ +import { preferenceService } from '@/core/services'; +import React from 'react'; +import { useCurrentAccount } from './account'; + +/** + * + * @description auto activate and inactivate last used account in screen + * + * @warning make sure only this hook can be ONLY called ONCE in nested components, + * or it will cause infinite loop. It's recommend to put it within the component + * holding in it + */ +export const useLastUsedAccountInScreen = (options?: { + disableAutoEffect?: boolean; +}) => { + const { disableAutoEffect = false } = options || {}; + const { currentAccount, switchAccount } = useCurrentAccount(); + + const doSwitchAccount = React.useCallback(() => { + const account = preferenceService.getCurrentAccount(); + if (account) { + switchAccount(account); + } + }, [switchAccount]); + + const activate = React.useCallback(async () => { + preferenceService.activateLastUsedAccount().then(doSwitchAccount); + }, [doSwitchAccount]); + + const inactivate = React.useCallback(() => { + preferenceService.inactivateLastUsedAccount(); + doSwitchAccount(); + }, [doSwitchAccount]); + + React.useEffect(() => { + if (disableAutoEffect) return; + + activate(); + return inactivate; + }, [disableAutoEffect, activate, inactivate]); + + return { + currentAccount, + activate, + inactivate, + }; +}; diff --git a/apps/mobile/src/navigation-type.ts b/apps/mobile/src/navigation-type.ts index 42af73adf..21396e4b1 100644 --- a/apps/mobile/src/navigation-type.ts +++ b/apps/mobile/src/navigation-type.ts @@ -48,6 +48,7 @@ export type BottomTabParamsList = { [RootNames.Home]?: {}; [RootNames.Dapps]?: {}; [RootNames.Points]?: {}; + /** @deprecated */ [RootNames.History]?: {}; [RootNames.Settings]?: { // enterActionType?: 'setBiometrics' | 'setAutoLockTime'; @@ -147,6 +148,7 @@ export type SingleAddressNavigatorParamList = { }; export type TransactionNavigatorParamList = { + [RootNames.History]?: {}; [RootNames.HistoryFilterScam]?: {}; [RootNames.Send]?: {}; [RootNames.SendNFT]?: { diff --git a/apps/mobile/src/screens/Bridge/index.tsx b/apps/mobile/src/screens/Bridge/index.tsx index 41ad6b037..72592ef4f 100644 --- a/apps/mobile/src/screens/Bridge/index.tsx +++ b/apps/mobile/src/screens/Bridge/index.tsx @@ -5,8 +5,11 @@ import { SettingVisibleProvider, } from './hooks'; import { BridgeContent } from './components/BridgeContent'; +import { useLastUsedAccountInScreen } from '@/hooks/useLastUsedAccountInScreen'; export const Bridge = () => { + useLastUsedAccountInScreen(); + return ( diff --git a/apps/mobile/src/screens/Dapps/DappsScreen/components/WebViewsStub.tsx b/apps/mobile/src/screens/Dapps/DappsScreen/components/WebViewsStub.tsx index a91de3546..3ba7a446b 100644 --- a/apps/mobile/src/screens/Dapps/DappsScreen/components/WebViewsStub.tsx +++ b/apps/mobile/src/screens/Dapps/DappsScreen/components/WebViewsStub.tsx @@ -280,7 +280,6 @@ export function OpenedDappWebViewStub() { useFocusEffect(onHardwareBackHandler); const { currentAccount } = useCurrentAccount(); - const { RcWalletIcon } = useWalletBrandLogo(currentAccount?.brandName); const { handleChange } = useAutoLockBottomSheetModalOnChange( handleBottomSheetChanges, @@ -295,6 +294,9 @@ export function OpenedDappWebViewStub() { const hasOpenedDapps = !!openedDappItems.length; + const lastUsedAccount = activeDapp?.currentAccount ?? currentAccount; + const { RcWalletIcon } = useWalletBrandLogo(lastUsedAccount?.brandName); + return ( (null); activeDappTabIdAtom.onMount = set => { @@ -106,6 +108,8 @@ export function useOpenDappView() { ); }, [toggleShowSheetModal]); + const dappLastUsedAccount = useDappLastUsedAccount(); + const openUrlAsDapp = useCallback( ( dappUrl: DappInfo['origin'] | OpenedDappItem, @@ -172,8 +176,11 @@ export function useOpenDappView() { setActiveDappOrigin(item.origin); } + dappLastUsedAccount.activate(dapps[item.origin]); + return true; }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ showDappWebViewModal, dapps, @@ -198,6 +205,8 @@ export function useOpenDappView() { const onHideActiveDapp = useCallback(() => { setActiveDappOrigin(null); + dappLastUsedAccount.inactivate(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [setActiveDappOrigin]); const closeActiveOpenedDapp = useCallback(() => { diff --git a/apps/mobile/src/screens/Home/HeaderArea.tsx b/apps/mobile/src/screens/Home/HeaderArea.tsx index d46791117..e32a583d9 100644 --- a/apps/mobile/src/screens/Home/HeaderArea.tsx +++ b/apps/mobile/src/screens/Home/HeaderArea.tsx @@ -18,9 +18,9 @@ import { Skeleton } from '@rneui/themed'; import { useCurve } from '@/hooks/useCurve'; import { splitNumberByStep } from '@/utils/number'; import TouchableView from '@/components/Touchable/TouchableView'; -import { useCurrentAccount } from '@/hooks/account'; +import { useAccounts, useCurrentAccount } from '@/hooks/account'; import { ellipsisAddress } from '@/utils/address'; -import { Text, Tip } from '@/components'; +import { Button, Text, Tip } from '@/components'; import { getWalletIcon } from '@/utils/walletInfo'; import { AppColorsVariants } from '@/constant/theme'; import { CommonSignal } from '@/components/WalletConnect/SessionSignal'; @@ -34,6 +34,8 @@ import { BottomSheetModal } from '@gorhom/bottom-sheet'; import usePrevious from 'ahooks/lib/usePrevious'; import useCachedValue from '@/hooks/common/useCachedValue'; import { GasAccountDashBoardHeader } from '../GasAccount/components/DashBoardHeader'; +import { MenuView } from '@react-native-menu/menu'; +import { preferenceService } from '@/core/services'; export default function HomeHeaderArea() { const { t } = useTranslation(); diff --git a/apps/mobile/src/screens/Home/MultiAddressHome.tsx b/apps/mobile/src/screens/Home/MultiAddressHome.tsx index dd8916ba3..d7746156c 100644 --- a/apps/mobile/src/screens/Home/MultiAddressHome.tsx +++ b/apps/mobile/src/screens/Home/MultiAddressHome.tsx @@ -210,7 +210,7 @@ function MultiAddressHome(): JSX.Element { break; case MultiHomeFeatTitle.History: navigation.dispatch( - StackActions.push(RootNames.StackRoot, { + StackActions.push(RootNames.StackTransaction, { screen: RootNames.History, params: {}, }), diff --git a/apps/mobile/src/screens/Navigators/TransactionNavigator.tsx b/apps/mobile/src/screens/Navigators/TransactionNavigator.tsx index 6019895fb..cc895c9a2 100644 --- a/apps/mobile/src/screens/Navigators/TransactionNavigator.tsx +++ b/apps/mobile/src/screens/Navigators/TransactionNavigator.tsx @@ -23,7 +23,11 @@ import ReceiveScreen from '../Receive/Receive'; import { GnosisTransactionQueue } from '../GnosisTransactionQueue'; import { Bridge } from '../Bridge'; import { GasAccountScreen } from '../GasAccount'; -import { ScreenHeaderAccountSwitcher } from '@/components/AccountSwitcher/OnScreenHeader'; +import { + ScreenHeaderAccountPicker, + ScreenHeaderAccountSwitcher, +} from '@/components/AccountSwitcher/OnScreenHeader'; +import HistoryScreen from '../Transaction/History'; const TransactionStack = createNativeStackNavigator(); @@ -79,11 +83,36 @@ export default function TransactionNavigator() { headerTransparent: true, })} /> + { + return ( + + ); + }, + }} + /> { + return ( + + ); + }, }} /> s.routes.find(r => r.name === RootNames.SendNFT)?.params, @@ -116,6 +119,7 @@ export default function SendNFT() { }, }}> + {/* FromToSection */} diff --git a/apps/mobile/src/screens/Swap/index.tsx b/apps/mobile/src/screens/Swap/index.tsx index 9fccb53e9..07442e6d5 100644 --- a/apps/mobile/src/screens/Swap/index.tsx +++ b/apps/mobile/src/screens/Swap/index.tsx @@ -56,8 +56,10 @@ import { MiniApproval } from '@/components/Approval/components/MiniSignTx/MiniSi import { KEYRING_CLASS, KEYRING_TYPE } from '@rabby-wallet/keyring-utils'; import { LowCreditModal, useLowCreditState } from './components/LowCreditModal'; import { AccountSwitcherModal } from '@/components/AccountSwitcher/Modal'; +import { useLastUsedAccountInScreen } from '@/hooks/useLastUsedAccountInScreen'; const Swap = () => { + useLastUsedAccountInScreen(); const { t } = useTranslation(); const { colors, styles } = useThemeStyles(getStyles); diff --git a/apps/mobile/src/screens/Transaction/History.tsx b/apps/mobile/src/screens/Transaction/History.tsx index ed7a97ad9..d54a2599e 100644 --- a/apps/mobile/src/screens/Transaction/History.tsx +++ b/apps/mobile/src/screens/Transaction/History.tsx @@ -29,6 +29,8 @@ import { HistoryList } from './components/HistoryList'; import { useCurrentAccount } from '@/hooks/account'; import RootScreenContainer from '@/components/ScreenContainer/RootScreenContainer'; import { ScreenSpecificStatusBar } from '@/components/FocusAwareStatusBar'; +import { useLastUsedAccountInScreen } from '@/hooks/useLastUsedAccountInScreen'; +import { AccountSwitcherModal } from '@/components/AccountSwitcher/Modal'; const PAGE_COUNT = 10; function History({ isTestnet = false }: { isTestnet?: boolean }): JSX.Element { @@ -188,8 +190,11 @@ const HistoryScreen = () => { const colors = useThemeColors(); const styles = getStyles(colors); + useLastUsedAccountInScreen(); + return ( + { const account = preferenceService.getCurrentAccount(); const address = account?.address; @@ -51,7 +56,9 @@ function HistoryFilterScamScreen(): JSX.Element { }; }; - const { data, loading } = useRequest(() => fetchData()); + const { data, loading } = useRequest(() => fetchData(), { + refreshDeps: [currentAccount], + }); if (!loading && !data?.list?.length) { return ; @@ -62,6 +69,7 @@ function HistoryFilterScamScreen(): JSX.Element { style={{ paddingBottom: bottom, }}> + {loading ? ( Loading may take a moment, and data delays are possible