Skip to content

Commit

Permalink
wallet-ext: account selector in tokens view (#7628)
Browse files Browse the repository at this point in the history
* under `wallet-multi-accounts` feature flag
* replaces `...` to `…` for ellipsis hook
* adds `address` to query key hash for the `useRecentTransactions` to
load transactions for the current active address
* changes the way we clear objects from store and now we do it from home
component until we move it to query

When only one account is available or `wallet-multi-accounts` is
disabled


https://user-images.githubusercontent.com/10210143/216132976-46070a62-85e8-4555-92eb-fb7d8102e61b.mov

For multi-accounts


https://user-images.githubusercontent.com/10210143/216133460-7cff027c-9c24-4037-8aeb-be4fa3acaf53.mov



https://user-images.githubusercontent.com/10210143/216133536-c87ba39b-c8f4-43ab-8154-b4735f8184a9.mov


closes: APPS-296
  • Loading branch information
pchrysochoidis authored Feb 3, 2023
1 parent b4d69b2 commit bbbdbec
Show file tree
Hide file tree
Showing 20 changed files with 248 additions and 31 deletions.
1 change: 1 addition & 0 deletions apps/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@
"@fontsource/red-hat-mono": "^4.5.11",
"@growthbook/growthbook": "^0.20.1",
"@growthbook/growthbook-react": "^0.10.1",
"@headlessui/react": "^1.7.7",
"@metamask/browser-passworder": "^4.0.2",
"@mysten/core": "workspace:*",
"@mysten/icons": "workspace:*",
Expand Down
13 changes: 13 additions & 0 deletions apps/wallet/src/background/keyring/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,19 @@ export class Keyring {
id
)
);
} else if (isKeyringPayload(payload, 'switchAccount')) {
if (this.#locked) {
throw new Error('Keyring is locked. Unlock it first.');
}
if (!payload.args) {
throw new Error('Missing parameters.');
}
const { address } = payload.args;
const changed = await this.changeActiveAccount(address);
if (!changed) {
throw new Error(`Failed to change account to ${address}`);
}
uiConnection.send(createMessage({ type: 'done' }, id));
}
} catch (e) {
uiConnection.send(
Expand Down
1 change: 1 addition & 0 deletions apps/wallet/src/shared/experimentation/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum FEATURES {
USE_TEST_NET_ENDPOINT = 'testnet-selection',
STAKING_ENABLED = 'wallet-staking-enabled',
WALLET_DAPPS = 'wallet-dapps',
WALLET_MULTI_ACCOUNTS = 'wallet-multi-accounts',
}

export function setAttributes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type MethodToPayloads = {
pubKey: string;
};
};
switchAccount: {
args: { address: SuiAddress };
return: void;
};
};

export interface KeyringPayload<Method extends keyof MethodToPayloads>
Expand Down
12 changes: 12 additions & 0 deletions apps/wallet/src/ui/app/background-client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,18 @@ export class BackgroundClient {
);
}

public async selectAccount(address: SuiAddress) {
return await lastValueFrom(
this.sendMessage(
createMessage<KeyringPayload<'switchAccount'>>({
type: 'keyring',
method: 'switchAccount',
args: { address },
})
).pipe(take(1))
);
}

private setupAppStatusUpdateInterval() {
setInterval(() => {
this.sendAppStatus();
Expand Down
24 changes: 24 additions & 0 deletions apps/wallet/src/ui/app/components/AccountList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useAccounts } from '../hooks/useAccounts';
import { AccountListItem, type AccountItemProps } from './AccountListItem';

export type AccountListProps = {
onAccountSelected: AccountItemProps['onAccountSelected'];
};

export function AccountList({ onAccountSelected }: AccountListProps) {
const allAccounts = useAccounts();
return (
<div className="flex flex-col items-stretch">
{allAccounts.map(({ address }) => (
<AccountListItem
address={address}
key={address}
onAccountSelected={onAccountSelected}
/>
))}
</div>
);
}
53 changes: 53 additions & 0 deletions apps/wallet/src/ui/app/components/AccountListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { Check12, Copy12 } from '@mysten/icons';

import { useMiddleEllipsis } from '../hooks';
import { useAccounts } from '../hooks/useAccounts';
import { useActiveAddress } from '../hooks/useActiveAddress';
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
import { Text } from '../shared/text';

import type { SuiAddress } from '@mysten/sui.js/src';

export type AccountItemProps = {
address: SuiAddress;
onAccountSelected: (address: SuiAddress) => void;
};

export function AccountListItem({
address,
onAccountSelected,
}: AccountItemProps) {
const account = useAccounts([address])[0];
const activeAddress = useActiveAddress();
const addressShort = useMiddleEllipsis(address);
const copy = useCopyToClipboard(address, {
copySuccessMessage: 'Address Copied',
});
if (!account) {
return null;
}
return (
<div
className="flex p-2.5 items-start gap-2.5 rounded-md hover:bg-sui/10 cursor-pointer focus-visible:ring-1 group transition-colors"
onClick={() => {
onAccountSelected(address);
}}
>
<div className="flex-1">
<Text color="steel-darker" variant="bodySmall" mono>
{addressShort}
</Text>
</div>
{activeAddress === address ? (
<Check12 className="text-success" />
) : null}
<Copy12
className="text-gray-60 group-hover:text-steel transition-colors hover:!text-hero-dark"
onClick={copy}
/>
</div>
);
}
85 changes: 85 additions & 0 deletions apps/wallet/src/ui/app/components/AccountSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { useFeature } from '@growthbook/growthbook-react';
import { Popover, Transition } from '@headlessui/react';
import { ChevronDown12, Copy12 } from '@mysten/icons';

import { useMiddleEllipsis } from '../hooks';
import { useAccounts } from '../hooks/useAccounts';
import { useActiveAddress } from '../hooks/useActiveAddress';
import { useBackgroundClient } from '../hooks/useBackgroundClient';
import { useCopyToClipboard } from '../hooks/useCopyToClipboard';
import { ButtonConnectedTo } from '../shared/ButtonConnectedTo';
import { Text } from '../shared/text';
import { AccountList } from './AccountList';
import { FEATURES } from '_src/shared/experimentation/features';

export function AccountSelector() {
const allAccounts = useAccounts();
const activeAddress = useActiveAddress();
const multiAccountsEnabled = useFeature(FEATURES.WALLET_MULTI_ACCOUNTS).on;
const activeAddressShort = useMiddleEllipsis(activeAddress);
const copyToAddress = useCopyToClipboard(activeAddressShort, {
copySuccessMessage: 'Address copied',
});
const backgroundClient = useBackgroundClient();
if (!allAccounts.length) {
return null;
}
const buttonText = (
<Text mono variant="bodySmall">
{activeAddressShort}
</Text>
);
if (!multiAccountsEnabled || allAccounts.length === 1) {
return (
<ButtonConnectedTo
text={buttonText}
onClick={copyToAddress}
iconAfter={<Copy12 />}
bgOnHover="grey"
/>
);
}
return (
<Popover className="relative z-10">
{({ close }) => (
<>
<Popover.Button
as={ButtonConnectedTo}
text={buttonText}
iconAfter={<ChevronDown12 />}
bgOnHover="grey"
/>
<Transition
enter="transition duration-200 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-200 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-75 opacity-0"
>
<Popover.Panel className="absolute left-1/2 -translate-x-1/2 w-50 drop-shadow-accountModal mt-2 z-0 rounded-md bg-white">
<div className="absolute w-3 h-3 bg-white -top-1 left-1/2 -translate-x-1/2 rotate-45" />
<div className="relative px-1.25 my-1.25 max-h-80 overflow-y-auto max-w-full z-10">
<AccountList
onAccountSelected={async (
selectedAddress
) => {
if (selectedAddress !== activeAddress) {
await backgroundClient.selectAccount(
selectedAddress
);
}
close();
}}
/>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
);
}
8 changes: 8 additions & 0 deletions apps/wallet/src/ui/app/hooks/useBackgroundClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

import { thunkExtras } from '../redux/store/thunk-extras';

export function useBackgroundClient() {
return thunkExtras.background;
}
2 changes: 1 addition & 1 deletion apps/wallet/src/ui/app/hooks/useMiddleEllipsis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function useMiddleEllipsis(
);
}
const endingLength = maxLength - beginningLength;
return `${txt.substring(0, beginningLength)}...${txt.substring(
return `${txt.substring(0, beginningLength)}${txt.substring(
txt.length - endingLength
)}`;
}, [txt, maxLength, maxLengthBeginning]);
Expand Down
2 changes: 1 addition & 1 deletion apps/wallet/src/ui/app/hooks/useRecentTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export function useRecentTransactions() {
const address = useAppSelector((state) => state.account.address);

return useQuery(
['transactions', 'recent'],
['transactions', 'recent', address],
async () => {
if (!address) return [];

Expand Down
17 changes: 14 additions & 3 deletions apps/wallet/src/ui/app/pages/home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { useEffect, useState } from 'react';
import { Outlet } from 'react-router-dom';
import { of, filter, switchMap, from, defer, repeat } from 'rxjs';

import { useActiveAddress } from '../../hooks/useActiveAddress';
import PageMainLayout from '_app/shared/page-main-layout';
import { useLockedGuard } from '_app/wallet/hooks';
import Loading from '_components/loading';
import { useInitializedGuard, useAppDispatch } from '_hooks';
import { useInitializedGuard, useAppDispatch, useAppSelector } from '_hooks';
import PageLayout from '_pages/layout';
import { fetchAllOwnedAndRequiredObjects } from '_redux/slices/sui-objects';
import {
clearSuiObjects,
fetchAllOwnedAndRequiredObjects,
} from '_redux/slices/sui-objects';
import { usePageView } from '_shared/utils';

const POLL_SUI_OBJECTS_INTERVAL = 4000;
Expand All @@ -28,6 +32,10 @@ const HomePage = ({ disableNavigation, limitToPopUpSize = true }: Props) => {
document?.visibilityState || null
);
const dispatch = useAppDispatch();
const activeAddress = useActiveAddress();
const network = useAppSelector(
({ app: { apiEnv, customRPC } }) => `${apiEnv}_${customRPC}`
);
useEffect(() => {
const callback = () => {
setVisibility(document.visibilityState);
Expand All @@ -38,6 +46,9 @@ const HomePage = ({ disableNavigation, limitToPopUpSize = true }: Props) => {
document.removeEventListener('visibilitychange', callback);
};
}, []);
useEffect(() => {
dispatch(clearSuiObjects());
}, [activeAddress, network, dispatch]);
useEffect(() => {
const sub = of(guardChecking || visibility === 'hidden')
.pipe(
Expand All @@ -50,7 +61,7 @@ const HomePage = ({ disableNavigation, limitToPopUpSize = true }: Props) => {
)
.subscribe();
return () => sub.unsubscribe();
}, [guardChecking, visibility, dispatch]);
}, [guardChecking, visibility, dispatch, activeAddress, network]);

usePageView();
return (
Expand Down
6 changes: 2 additions & 4 deletions apps/wallet/src/ui/app/pages/home/tokens/TokensDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import CoinBalance from './coin-balance';
import IconLink from './icon-link';
import FaucetRequestButton from '_app/shared/faucet/request-button';
import PageTitle from '_app/shared/page-title';
import AccountAddress from '_components/account-address';
import Alert from '_components/alert';
import Loading from '_components/loading';
import RecentTransactions from '_components/transactions-card/RecentTransactions';
import { SuiIcons } from '_font-icons/output/sui-icons';
import { useAppSelector, useObjectsState } from '_hooks';
import { accountAggregateBalancesSelector } from '_redux/slices/account';
import { GAS_TYPE_ARG, Coin } from '_redux/slices/sui-objects/Coin';
import { AccountSelector } from '_src/ui/app/components/AccountSelector';

import st from './TokensPage.module.scss';

Expand Down Expand Up @@ -112,9 +112,7 @@ function TokenDetails({ coinType }: TokenDetailsProps) {
<small>{error.message}</small>
</Alert>
) : null}
{!coinType && (
<AccountAddress showLink={false} copyable mode="faded" />
)}
{!coinType && <AccountSelector />}
<div className={st.balanceContainer}>
<Loading loading={loading}>
<CoinBalance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
height: 100%;
flex-grow: 1;
align-items: center;
overflow-y: scroll;
margin-top: 20px;

@include utils.override-main-padding;
}
Expand Down
16 changes: 2 additions & 14 deletions apps/wallet/src/ui/app/redux/slices/app/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

import { AppType } from './AppType';
import { DEFAULT_API_ENV } from '_app/ApiProvider';
import {
clearForNetworkSwitch,
fetchAllOwnedAndRequiredObjects,
} from '_redux/slices/sui-objects';

import type { PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from '_redux/RootReducer';
Expand All @@ -19,7 +15,6 @@ import type { AppThunkConfig } from '_store/thunk-extras';
type AppState = {
appType: AppType;
apiEnv: API_ENV;
apiEnvInitialized: boolean;
navVisible: boolean;
customRPC?: string | null;
activeOrigin: string | null;
Expand All @@ -29,7 +24,6 @@ type AppState = {
const initialState: AppState = {
appType: AppType.unknown,
apiEnv: DEFAULT_API_ENV,
apiEnvInitialized: false,
customRPC: null,
navVisible: true,
activeOrigin: null,
Expand All @@ -44,18 +38,13 @@ export const changeActiveNetwork = createAsyncThunk<
'changeRPCNetwork',
async (
{ network, store = false },
{ extra: { background, api }, dispatch, getState }
{ extra: { background, api }, dispatch }
) => {
if (store) {
await background.setActiveNetworkEnv(network);
}
const { apiEnvInitialized } = getState().app;
await dispatch(slice.actions.setActiveNetwork(network));
api.setNewJsonRpcProvider(network.env, network.customRpcUrl);
if (apiEnvInitialized) {
await dispatch(clearForNetworkSwitch());
dispatch(fetchAllOwnedAndRequiredObjects());
}
await dispatch(slice.actions.setActiveNetwork(network));
}
);

Expand All @@ -71,7 +60,6 @@ const slice = createSlice({
) => {
state.apiEnv = env;
state.customRPC = customRpcUrl;
state.apiEnvInitialized = true;
},
setNavVisibility: (
state,
Expand Down
Loading

3 comments on commit bbbdbec

@vercel
Copy link

@vercel vercel bot commented on bbbdbec Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

explorer – ./apps/explorer

explorer-topaz.vercel.app
explorer.sui.io
explorer-mysten-labs.vercel.app
explorer-git-main-mysten-labs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on bbbdbec Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

frenemies – ./dapps/frenemies

frenemies-git-main-mysten-labs.vercel.app
frenemies.vercel.app
frenemies-mysten-labs.vercel.app

@vercel
Copy link

@vercel vercel bot commented on bbbdbec Feb 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

wallet-adapter – ./sdk/wallet-adapter/example

Please sign in to comment.