Skip to content

feat(solana): update add account from opt in solana #31387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions ui/components/app/whats-new-modal/solana/modal-footer.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import React from 'react';
import { useSelector } from 'react-redux';
import {
ModalFooter as BaseModalFooter,
Button,
ButtonSize,
ButtonVariant,
} from '../../../component-library';
import { useI18nContext } from '../../../../hooks/useI18nContext';
import {
WalletClientType,
useMultichainWalletSnapClient,
} from '../../../../hooks/accounts/useMultichainWalletSnapClient';
import { MultichainNetworks } from '../../../../../shared/constants/multichain/networks';
import { getMetaMaskKeyrings } from '../../../../selectors';

type ModalFooterProps = {
onAction: () => void;
Expand All @@ -21,10 +14,6 @@ type ModalFooterProps = {

export const SolanaModalFooter = ({ onAction, onCancel }: ModalFooterProps) => {
const t = useI18nContext();
const solanaWalletSnapClient = useMultichainWalletSnapClient(
WalletClientType.Solana,
);
const [primaryKeyring] = useSelector(getMetaMaskKeyrings);

return (
<BaseModalFooter paddingTop={4} data-testid="solana-modal-footer">
Expand All @@ -33,14 +22,7 @@ export const SolanaModalFooter = ({ onAction, onCancel }: ModalFooterProps) => {
size={ButtonSize.Md}
variant={ButtonVariant.Primary}
data-testid="create-solana-account-button"
onClick={async () => {
onAction();

await solanaWalletSnapClient.createAccount({
scope: MultichainNetworks.SOLANA,
entropySource: primaryKeyring.metadata.id,
});
}}
onClick={onAction}
>
{t('createSolanaAccount')}
</Button>
Expand Down
67 changes: 65 additions & 2 deletions ui/components/app/whats-new-modal/whats-new-modal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ jest.mock('../../../hooks/accounts/useMultichainWalletSnapClient', () => ({
},
}));

jest.mock('../../../store/actions', () => ({
...jest.requireActual('../../../store/actions'),
getNextAvailableAccountName: () => 'Test Account',
}));

describe('WhatsNewModal', () => {
const mockOnClose = jest.fn();
const mockCreateAccount = jest.fn();
const KEYRING_ID = '01JKAF3DSGM3AB87EM9N0K41AJ';
const MOCK_ADDRESS = '0x1234567890123456789012345678901234567891';

beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -41,11 +47,55 @@ describe('WhatsNewModal', () => {
},
keyrings: [
{
accounts: [MOCK_ADDRESS],
metadata: {
id: KEYRING_ID,
},
},
],
internalAccounts: {
accounts: {
[KEYRING_ID]: {
address: MOCK_ADDRESS,
id: KEYRING_ID,
metadata: {
name: 'Account 1',
keyring: {
type: 'HD Key Tree',
},
},
options: {},
methods: [
'personal_sign',
'eth_sign',
'eth_signTransaction',
'eth_signTypedData_v1',
'eth_signTypedData_v3',
'eth_signTypedData_v4',
],
type: 'eip155:eoa',
},
},
selectedAccount: KEYRING_ID,
},
accounts: {
[MOCK_ADDRESS]: {
address: MOCK_ADDRESS,
balance: '0x0',
nonce: '0x0',
code: '0x',
},
},
accountsByChainId: {
'0x5': {
[MOCK_ADDRESS]: {
address: MOCK_ADDRESS,
balance: '0x0',
nonce: '0x0',
code: '0x',
},
},
},
},
});
return renderWithProvider(<WhatsNewModal onClose={mockOnClose} />, store);
Expand Down Expand Up @@ -73,13 +123,26 @@ describe('WhatsNewModal', () => {
).toBeInTheDocument();
});

it('calls createAccount when clicking the create account button', async () => {
it('opens the create solana account modal and handles account creation', async () => {
const createButton = screen.getByTestId('create-solana-account-button');
fireEvent.click(createButton);

expect(mockCreateAccount).toHaveBeenCalledWith({
expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument();

expect(
screen.getByTestId('create-solana-account-modal'),
).toBeInTheDocument();

const accountNameInput = screen.getByLabelText(/account name/iu);
fireEvent.change(accountNameInput, { target: { value: 'Test Account' } });

const submitButton = screen.getByTestId('submit-add-account-with-name');
fireEvent.click(submitButton);

await expect(mockCreateAccount).toHaveBeenCalledWith({
scope: MultichainNetworks.SOLANA,
entropySource: KEYRING_ID,
accountNameSuggestion: 'Test Account',
});
expect(mockOnClose).toHaveBeenCalled();
});
Expand Down
64 changes: 42 additions & 22 deletions ui/components/app/whats-new-modal/whats-new-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useContext } from 'react';
import React, { useContext, useState } from 'react';
import { useSelector } from 'react-redux';

import {
Display,
FlexDirection,
} from '../../../helpers/constants/design-system';
import { ModalOverlay, ModalContent, Modal } from '../../component-library';
import { CreateSolanaAccountModal } from '../../multichain/create-solana-account-modal';

import {
MetaMetricsEventCategory,
Expand Down Expand Up @@ -48,12 +49,14 @@ type RenderNotificationProps = {
notification: NotificationType;
onClose: () => void;
onNotificationViewed: (id: number) => void;
onCreateSolanaAccount: () => void;
};

const renderNotification = ({
notification,
onClose,
onNotificationViewed,
onCreateSolanaAccount,
}: RenderNotificationProps) => {
const { id, title, image, modal } = notification;

Expand All @@ -76,7 +79,7 @@ const renderNotification = ({
{modal?.body && <modal.body.component title={title} />}
{modal?.footer && (
<modal.footer.component
onAction={handleNotificationClose}
onAction={onCreateSolanaAccount}
onCancel={handleNotificationClose}
/>
)}
Expand All @@ -87,6 +90,8 @@ const renderNotification = ({
export default function WhatsNewModal({ onClose }: WhatsNewModalProps) {
const t = useContext(I18nContext);
const trackEvent = useContext(MetaMetricsContext);
const [showCreateSolanaAccountModal, setShowCreateSolanaAccountModal] =
useState(false);

const notifications = useSelector(getSortedAnnouncementsToShow);

Expand All @@ -106,26 +111,41 @@ export default function WhatsNewModal({ onClose }: WhatsNewModalProps) {
onClose();
};

const handleCreateSolanaAccount = () => {
setShowCreateSolanaAccountModal(true);
};

return (
<Modal
onClose={handleModalClose}
data-testid="whats-new-modal"
isOpen={notifications.length > 0}
isClosedOnOutsideClick
isClosedOnEscapeKey
autoFocus={false}
>
<ModalOverlay />

{notifications.map(({ id }) => {
const notification = getTranslatedUINotifications(t)[id];

return renderNotification({
notification,
onClose,
onNotificationViewed: handleNotificationViewed,
});
})}
</Modal>
<>
<Modal
onClose={() => null}
data-testid="whats-new-modal"
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this null? Do we not want to allow the user to close this via the x?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do, but it's done with:

<modal.header.component onClose={onClose} image={image} />

I changed it to null, because it was also triggered by some other flow, where we didn't want to close it yet.

isOpen={notifications.length > 0 && !showCreateSolanaAccountModal}
isClosedOnOutsideClick
isClosedOnEscapeKey
autoFocus={false}
>
<ModalOverlay />

{notifications.map(({ id }) => {
const notification = getTranslatedUINotifications(t)[id];

return renderNotification({
notification,
onClose,
onNotificationViewed: handleNotificationViewed,
onCreateSolanaAccount: handleCreateSolanaAccount,
});
})}
</Modal>
{showCreateSolanaAccountModal && (
<CreateSolanaAccountModal
onClose={() => {
setShowCreateSolanaAccountModal(false);
handleModalClose();
}}
/>
)}
</>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this in parallel with the normal notifications Modal?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well it technically is a part of the flow from the notification modal, but I didn't want to use the notification modal itself, as it's a bit limited in terms of the structure (header, content and footer) and it was easier to add it next to it and just hide notification when user decides to add new account.

);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesnt this already exist as create-snap-account?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually this one is using create-snap-account, it's just a modal that uses it, it was needed as we don't have modal open, as we had in case of network or account modal flow.

import { useSelector } from 'react-redux';
import {
Box,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
} from '../../component-library';
import {
Display,
FlexDirection,
} from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { getMetaMaskKeyrings } from '../../../selectors';
import { CreateSnapAccount } from '../create-snap-account';
import { SrpList } from '../multi-srp/srp-list';
import { WalletClientType } from '../../../hooks/accounts/useMultichainWalletSnapClient';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';

type CreateSolanaAccountModalProps = {
onClose: () => void;
};

export const CreateSolanaAccountModal = ({
onClose,
}: CreateSolanaAccountModalProps) => {
const t = useI18nContext();
const [primaryKeyring] = useSelector(getMetaMaskKeyrings);
const [showSrpSelection, setShowSrpSelection] = React.useState(false);
const [showCreateAccount, setShowCreateAccount] = React.useState(true);
const [selectedKeyringId, setSelectedKeyringId] = React.useState(
primaryKeyring.metadata.id,
);

if (showCreateAccount) {
return (
<Modal isOpen onClose={onClose}>
<ModalOverlay />
<ModalContent
className="create-solana-account-modal"
data-testid="create-solana-account-modal"
modalDialogProps={{
className: 'create-solana-account-modal__dialog',
padding: 0,
display: Display.Flex,
flexDirection: FlexDirection.Column,
}}
>
<ModalHeader padding={4} onClose={onClose}>
{t('createSolanaAccount')}
</ModalHeader>
<Box paddingLeft={4} paddingRight={4} paddingBottom={4}>
<CreateSnapAccount
onActionComplete={async (confirmed: boolean) => {
if (confirmed) {
onClose();
} else {
setShowCreateAccount(false);
}
}}
selectedKeyringId={selectedKeyringId}
onSelectSrp={() => {
setShowSrpSelection(true);
setShowCreateAccount(false);
}}
clientType={WalletClientType.Solana}
chainId={MultichainNetworks.SOLANA}
/>
</Box>
</ModalContent>
</Modal>
);
}

if (showSrpSelection) {
return (
<Modal isOpen onClose={onClose}>
<ModalOverlay />
<ModalContent
className="create-solana-account-modal"
data-testid="create-solana-account-modal"
modalDialogProps={{
className: 'create-solana-account-modal__dialog',
padding: 0,
display: Display.Flex,
flexDirection: FlexDirection.Column,
}}
>
<ModalHeader padding={4} onClose={onClose}>
{t('selectSRP')}
</ModalHeader>
<Box paddingLeft={4} paddingRight={4} paddingBottom={4}>
<SrpList
onActionComplete={(keyringId: string) => {
setSelectedKeyringId(keyringId);
setShowCreateAccount(true);
}}
/>
</Box>
</ModalContent>
</Modal>
);
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CreateSolanaAccountModal } from './create-solana-account-modal';
Loading