Skip to content

Commit bc1cdf9

Browse files
author
Shawn
authored
feat: show used addresses and next unused address in receive box (#1368)
* feat: show used addresses and next unused address in receive box * refactor: add flag to button * fix: revert incorrect package bump * refactor: change feat flag name * fix: feature flag not casting correctly * fix: typo in env variable * fix: failing unit test in staking modal
1 parent 01d7f47 commit bc1cdf9

File tree

7 files changed

+109
-23
lines changed

7 files changed

+109
-23
lines changed

apps/browser-extension-wallet/.env.defaults

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ USE_MULTI_DELEGATION_STAKING_GRID_VIEW=true
2828
USE_MULTI_DELEGATION_STAKING_FILTERS=false
2929
USE_ROS_STAKING_COLUMN=false
3030
USE_FOOR_TOPUP=true
31+
USE_ADVANCED_RECEIVED=false
3132

3233
USE_POSTHOG_ANALYTICS_FOR_OPTED_OUT=false
3334
USE_MULTI_WALLET=true

apps/browser-extension-wallet/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ export * from './useWalletAvatar';
2626
export * from './useActionExecution';
2727
export * from './useCustomSubmitApi';
2828
export * from './useSharedWalletData';
29+
export * from './useNextUnusedAddress';
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Cardano } from '@cardano-sdk/core';
2+
import { useWalletStore } from '@src/stores';
3+
import { useEffect, useState } from 'react';
4+
5+
export const useNextUnusedAddress = (): Cardano.PaymentAddress => {
6+
const [nextUnusedAddress, setNextUnusedAddress] = useState<Cardano.PaymentAddress>();
7+
const { inMemoryWallet } = useWalletStore();
8+
9+
useEffect(() => {
10+
if (!inMemoryWallet) {
11+
return;
12+
}
13+
14+
const getNextUnusedAddress = async () => {
15+
const _nextUnusedAddress = await inMemoryWallet.getNextUnusedAddress();
16+
setNextUnusedAddress(_nextUnusedAddress?.[0].address);
17+
};
18+
19+
getNextUnusedAddress();
20+
}, [inMemoryWallet]);
21+
22+
return nextUnusedAddress;
23+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Cardano } from '@cardano-sdk/core';
2+
3+
export const isUsedAddress = (
4+
address: Cardano.PaymentAddress,
5+
transactionHistory: Cardano.HydratedTx[]
6+
): Cardano.HydratedTx => transactionHistory.find((tx) => tx.body.outputs.find((output) => output.address === address));

apps/browser-extension-wallet/src/views/browser-view/components/QRInfoWalletDrawer/QRInfoWalletDrawer.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
}
88
}
99

10-
.draweHeaderTitle {
10+
.drawerHeaderTitle {
1111
display: flex;
1212
flex-direction: column;
1313
gap: size_unit(2);
Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { ADDRESS_CARD_QR_CODE_SIZE, AddressCard, HandleAddressCard } from '@lace/core';
33
import { useTheme } from '@providers/ThemeProvider';
44
import { useWalletStore } from '@src/stores';
@@ -11,20 +11,36 @@ import { useGetHandles } from '@hooks/useGetHandles';
1111
import { getAssetImageUrl } from '@src/utils/get-asset-image-url';
1212
import { useAnalyticsContext } from '@providers';
1313
import { PostHogAction } from '@providers/AnalyticsProvider/analyticsTracker';
14+
import { Button, Flex } from '@input-output-hk/lace-ui-toolkit';
15+
import { PlusOutlined } from '@ant-design/icons';
16+
import { isUsedAddress } from '@src/utils/is-used-addresses';
17+
import { useNextUnusedAddress } from '@hooks';
18+
19+
const useAdvancedReceived = process.env.USE_ADVANCED_RECEIVED === 'true';
1420

1521
const useWalletInformation = () =>
1622
useWalletStore((state) => ({
1723
name: state?.walletInfo?.name,
18-
address: state?.walletInfo?.addresses[0].address
24+
addresses: state?.walletInfo?.addresses
1925
}));
2026

27+
const useTransactionHistory = () =>
28+
useWalletStore((state) => ({
29+
transactionHistory: state?.walletState?.transactions.history
30+
}));
31+
32+
const addressCopiedTranslation = 'core.infoWallet.addressCopied';
33+
2134
export const QRInfoWalletDrawer = (): React.ReactElement => {
2235
const analytics = useAnalyticsContext();
2336
const { t } = useTranslation();
2437
const { theme } = useTheme();
25-
const { name, address } = useWalletInformation();
38+
const { name, addresses } = useWalletInformation();
39+
40+
const { transactionHistory } = useTransactionHistory();
2641
const [, closeDrawer] = useDrawer();
2742
const handles = useGetHandles();
43+
const nextUnusedAddress = useNextUnusedAddress();
2844

2945
useKeyboardShortcut(['Escape'], () => closeDrawer());
3046

@@ -36,24 +52,61 @@ export const QRInfoWalletDrawer = (): React.ReactElement => {
3652
analytics.sendEventToPostHog(PostHogAction.ReceiveCopyAddressIconClick);
3753
};
3854

55+
const usedAddresses = useMemo(
56+
() => addresses?.filter((addr) => isUsedAddress(addr.address, transactionHistory)),
57+
[addresses, transactionHistory]
58+
);
59+
60+
const getQRCodeOpts = useCallback(() => getQRCodeOptions(theme, ADDRESS_CARD_QR_CODE_SIZE), [theme]);
61+
3962
return (
40-
<div className={styles.infoContainer}>
41-
<AddressCard
42-
name={name}
43-
address={address?.toString()}
44-
getQRCodeOptions={useCallback(() => getQRCodeOptions(theme, ADDRESS_CARD_QR_CODE_SIZE), [theme])}
45-
copiedMessage={t('core.infoWallet.addressCopied')}
46-
onCopyClick={handleCopyAddress}
47-
/>
48-
{handles?.map(({ nftMetadata, image }) => (
49-
<HandleAddressCard
50-
key={nftMetadata.name}
51-
name={nftMetadata.name}
52-
image={getAssetImageUrl(image || nftMetadata.image)}
53-
copiedMessage={t('core.infoWallet.handleCopied')}
54-
onCopyClick={handleCopyAdaHandle}
55-
/>
56-
))}
57-
</div>
63+
<Flex flexDirection="column" justifyContent="space-between" alignItems="center">
64+
<div className={styles.infoContainer}>
65+
{!useAdvancedReceived ? (
66+
<AddressCard
67+
name={name}
68+
address={addresses?.[0].address}
69+
getQRCodeOptions={getQRCodeOpts}
70+
copiedMessage={t(addressCopiedTranslation)}
71+
onCopyClick={handleCopyAddress}
72+
/>
73+
) : (
74+
<>
75+
{usedAddresses?.map((addr, i) => (
76+
<AddressCard
77+
key={addr.accountIndex}
78+
name={i === 0 ? name : `Index ${i}`}
79+
address={addr.address}
80+
getQRCodeOptions={getQRCodeOpts}
81+
copiedMessage={t(addressCopiedTranslation)}
82+
onCopyClick={handleCopyAddress}
83+
/>
84+
))}
85+
{nextUnusedAddress && (
86+
<AddressCard
87+
name={'Next Unused Address'}
88+
address={nextUnusedAddress}
89+
getQRCodeOptions={getQRCodeOpts}
90+
copiedMessage={t(addressCopiedTranslation)}
91+
onCopyClick={handleCopyAddress}
92+
/>
93+
)}
94+
</>
95+
)}
96+
{handles?.map(({ nftMetadata, image }) => (
97+
<HandleAddressCard
98+
key={nftMetadata.name}
99+
name={nftMetadata.name}
100+
image={getAssetImageUrl(image || nftMetadata.image)}
101+
copiedMessage={t('core.infoWallet.handleCopied')}
102+
onCopyClick={handleCopyAdaHandle}
103+
/>
104+
))}
105+
</div>
106+
{/* TODO: onClick to generate visible unused address, translation */}
107+
{useAdvancedReceived && (
108+
<Button.Secondary icon={<PlusOutlined />} onClick={() => void 0} label="Create new address" />
109+
)}
110+
</Flex>
58111
);
59112
};

apps/browser-extension-wallet/src/views/browser-view/features/staking/components/StakingModals/__tests__/StakingModals.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ jest.mock('../../../store', () => ({
2525
const handles$ = new BehaviorSubject([]);
2626

2727
const inMemoryWallet = {
28-
handles$
28+
handles$,
29+
getNextUnusedAddress: jest.fn()
2930
};
31+
3032
jest.mock('@src/stores', () => ({
3133
...jest.requireActual<any>('@src/stores'),
3234
useWalletStore: mockUseWalletStore

0 commit comments

Comments
 (0)