Skip to content

Commit 5c349b2

Browse files
fix(whats-new-modal): display different message for users with a Solana account already created (#31318)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** Adds a different text for Solana's "What's New" modal if the user already has a Solana account. Adds MetaMetric events for the different user flows: if they choose to create a Solana account, if they already have one, if they dismiss the modal [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/31318?quickstart=1) ## **Related issues** Fixes: [SOL-217](https://consensyssoftware.atlassian.net/browse/SOL-217?atlOrigin=eyJpIjoiNTdhMzFlYzhiZWJkNDg3Zjg5MzM0NDRhOGY3ZGM2MTQiLCJwIjoiaiJ9) ## **Manual testing steps** 1. Fresh install the extension 2. The first page should show a pop up with "Create Solana Account" 3. When the action item is clicked on it should open the Solana Snap's create account flow ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. [SOL-217]: https://consensyssoftware.atlassian.net/browse/SOL-217?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ --------- Co-authored-by: António Regadas <[email protected]> Co-authored-by: Antonio Regadas <[email protected]>
1 parent 99c4e5d commit 5c349b2

File tree

9 files changed

+222
-83
lines changed

9 files changed

+222
-83
lines changed

app/scripts/lib/snap-keyring/keyring-snaps-permissions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import { KeyringRpcMethod } from '@metamask/keyring-api';
12
import {
2-
SubjectType,
33
SubjectMetadataController,
4+
SubjectType,
45
} from '@metamask/permission-controller';
5-
import { KeyringRpcMethod } from '@metamask/keyring-api';
66

77
/**
88
* The origins of the Portfolio dapp.

shared/notifications/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export type ModalHeaderProps = {
2626
image?: NotificationImage;
2727
};
2828
export type ModalBodyProps = { title: string };
29-
export type ModalFooterProps = { onAction: () => void; onCancel: () => void };
29+
export type ModalFooterProps = {
30+
onAction: () => void | Promise<void>;
31+
onCancel: () => void | Promise<void>;
32+
};
3033

3134
export type TranslatedUINotification = {
3235
id: number;

ui/components/app/whats-new-modal/notifications.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {
2-
TranslationFunction,
2+
NOTIFICATION_SOLANA_ON_METAMASK,
33
TranslatedUINotifications,
4+
TranslationFunction,
45
UI_NOTIFICATIONS,
5-
NOTIFICATION_SOLANA_ON_METAMASK,
66
} from '../../../../shared/notifications';
77
import {
8-
SolanaModalHeader,
98
SolanaModalBody,
109
SolanaModalFooter,
10+
SolanaModalHeader,
1111
} from './solana';
1212

1313
export const getTranslatedUINotifications = (

ui/components/app/whats-new-modal/solana/modal-footer.tsx

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
1-
import React from 'react';
1+
import React, { useContext } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import {
4+
MetaMetricsEventCategory,
5+
MetaMetricsEventName,
6+
} from '../../../../../shared/constants/metametrics';
7+
import { ModalFooterProps } from '../../../../../shared/notifications';
8+
import { MetaMetricsContext } from '../../../../contexts/metametrics';
9+
import { useI18nContext } from '../../../../hooks/useI18nContext';
10+
import { hasCreatedSolanaAccount } from '../../../../selectors';
211
import {
312
ModalFooter as BaseModalFooter,
413
Button,
514
ButtonSize,
615
ButtonVariant,
716
} from '../../../component-library';
8-
import { useI18nContext } from '../../../../hooks/useI18nContext';
917

10-
type ModalFooterProps = {
11-
onAction: () => void;
12-
onCancel: () => void;
13-
};
18+
const SOLANA_FEATURE = 'solana';
19+
const CREATE_SOLANA_ACCOUNT_ACTION = 'create-solana-account';
20+
const GOT_IT_ACTION = 'got-it';
1421

1522
export const SolanaModalFooter = ({ onAction, onCancel }: ModalFooterProps) => {
1623
const t = useI18nContext();
24+
const hasSolanaAccount = useSelector(hasCreatedSolanaAccount);
25+
const trackEvent = useContext(MetaMetricsContext);
26+
27+
const handleCreateSolanaAccount = async () => {
28+
trackEvent({
29+
category: MetaMetricsEventCategory.Onboarding,
30+
event: MetaMetricsEventName.WhatsNewClicked,
31+
properties: {
32+
feature: SOLANA_FEATURE,
33+
action: CREATE_SOLANA_ACCOUNT_ACTION,
34+
},
35+
});
36+
await onAction();
37+
};
38+
39+
const handleGotIt = async () => {
40+
trackEvent({
41+
category: MetaMetricsEventCategory.Onboarding,
42+
event: MetaMetricsEventName.WhatsNewClicked,
43+
properties: {
44+
feature: SOLANA_FEATURE,
45+
action: GOT_IT_ACTION,
46+
},
47+
});
48+
onCancel();
49+
};
1750

1851
return (
1952
<BaseModalFooter paddingTop={4} data-testid="solana-modal-footer">
2053
<Button
2154
block
2255
size={ButtonSize.Md}
2356
variant={ButtonVariant.Primary}
24-
data-testid="create-solana-account-button"
25-
onClick={onAction}
57+
data-testid={
58+
hasSolanaAccount ? 'got-it-button' : 'create-solana-account-button'
59+
}
60+
onClick={hasSolanaAccount ? handleGotIt : handleCreateSolanaAccount}
2661
>
27-
{t('createSolanaAccount')}
62+
{hasSolanaAccount ? t('gotIt') : t('createSolanaAccount')}
2863
</Button>
2964
<Button
3065
block

ui/components/app/whats-new-modal/solana/modal-header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
22
import {
3+
AlignItems,
34
Display,
45
FlexDirection,
5-
AlignItems,
66
} from '../../../../helpers/constants/design-system';
77
import {
88
ModalHeader as BaseModalHeader,

ui/components/app/whats-new-modal/whats-new-modal.test.tsx

Lines changed: 147 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { fireEvent, screen, waitFor } from '@testing-library/react';
12
import React from 'react';
2-
import { screen, fireEvent } from '@testing-library/react';
3-
import { renderWithProvider } from '../../../../test/jest';
4-
import configureStore from '../../../store/store';
5-
import mockState from '../../../../test/data/mock-state.json';
3+
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
64
import { NOTIFICATION_SOLANA_ON_METAMASK } from '../../../../shared/notifications';
5+
import { MOCK_ACCOUNT_SOLANA_MAINNET } from '../../../../test/data/mock-accounts';
6+
import mockState from '../../../../test/data/mock-state.json';
7+
import { renderWithProvider } from '../../../../test/jest';
78
import { useMultichainWalletSnapClient } from '../../../hooks/accounts/useMultichainWalletSnapClient';
8-
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
9+
import configureStore from '../../../store/store';
910
import WhatsNewModal from './whats-new-modal';
1011

1112
jest.mock('../../../hooks/accounts/useMultichainWalletSnapClient', () => ({
@@ -34,7 +35,11 @@ describe('WhatsNewModal', () => {
3435
});
3536
});
3637

37-
const renderModalWithNotification = (notificationId: number) => {
38+
const renderModalWithNotification = ({
39+
notificationId,
40+
}: {
41+
notificationId: number;
42+
}) => {
3843
const store = configureStore({
3944
metamask: {
4045
...mockState.metamask,
@@ -102,55 +107,153 @@ describe('WhatsNewModal', () => {
102107
};
103108

104109
describe('Whats new notification modal', () => {
105-
beforeEach(() => {
106-
renderModalWithNotification(NOTIFICATION_SOLANA_ON_METAMASK);
107-
});
110+
describe('Content agnostic functionality', () => {
111+
beforeEach(() => {
112+
renderModalWithNotification({
113+
notificationId: NOTIFICATION_SOLANA_ON_METAMASK,
114+
});
115+
});
108116

109-
it('calls onClose when the modal is closed', () => {
110-
const closeButton = screen.getByRole('button', { name: /close/iu });
111-
fireEvent.click(closeButton);
112-
expect(mockOnClose).toHaveBeenCalled();
113-
});
117+
it('calls onClose when the modal is closed', async () => {
118+
const closeButton = screen.getByRole('button', { name: /close/iu });
119+
fireEvent.click(closeButton);
114120

115-
it('renders Solana notification content correctly', () => {
116-
expect(screen.getByTestId('solana-modal-body')).toBeInTheDocument();
117-
expect(
118-
screen.getByText(/Send, receive, and swap tokens/iu),
119-
).toBeInTheDocument();
120-
expect(screen.getByText(/Import Solana accounts/iu)).toBeInTheDocument();
121-
expect(
122-
screen.getByText(/More features coming soon/iu),
123-
).toBeInTheDocument();
121+
await waitFor(() => {
122+
expect(mockOnClose).toHaveBeenCalled();
123+
});
124+
});
124125
});
125126

126-
it('opens the create solana account modal and handles account creation', async () => {
127-
const createButton = screen.getByTestId('create-solana-account-button');
128-
fireEvent.click(createButton);
127+
describe('Solana notification content', () => {
128+
describe('when the user does not have a Solana account', () => {
129+
beforeEach(() => {
130+
renderModalWithNotification({
131+
notificationId: NOTIFICATION_SOLANA_ON_METAMASK,
132+
});
133+
});
134+
135+
it('renders Solana notification when the user does not have a Solana account', () => {
136+
expect(screen.getByTestId('solana-modal-body')).toBeInTheDocument();
137+
expect(
138+
screen.getByText(/Send, receive, and swap tokens/iu),
139+
).toBeInTheDocument();
140+
expect(
141+
screen.getByText(/Import Solana accounts/iu),
142+
).toBeInTheDocument();
143+
expect(
144+
screen.getByText(/More features coming soon/iu),
145+
).toBeInTheDocument();
146+
expect(
147+
screen.getByTestId('create-solana-account-button'),
148+
).toBeInTheDocument();
149+
expect(screen.getByTestId('not-now-button')).toBeInTheDocument();
150+
});
151+
152+
it('opens the "Create account" modal when clicking the "Create account" button', async () => {
153+
const createButton = screen.getByTestId(
154+
'create-solana-account-button',
155+
);
156+
fireEvent.click(createButton);
157+
158+
expect(
159+
screen.queryByTestId('whats-new-modal'),
160+
).not.toBeInTheDocument();
161+
162+
expect(
163+
screen.getByTestId('create-solana-account-modal'),
164+
).toBeInTheDocument();
129165

130-
expect(screen.queryByTestId('whats-new-modal')).not.toBeInTheDocument();
166+
// TODO: The next code should be tested in the CreateSolanaAccountModal component
131167

132-
expect(
133-
screen.getByTestId('create-solana-account-modal'),
134-
).toBeInTheDocument();
168+
const accountNameInput = screen.getByLabelText(/account name/iu);
169+
fireEvent.change(accountNameInput, {
170+
target: { value: 'Test Account' },
171+
});
135172

136-
const accountNameInput = screen.getByLabelText(/account name/iu);
137-
fireEvent.change(accountNameInput, { target: { value: 'Test Account' } });
173+
const submitButton = screen.getByTestId(
174+
'submit-add-account-with-name',
175+
);
176+
fireEvent.click(submitButton);
138177

139-
const submitButton = screen.getByTestId('submit-add-account-with-name');
140-
fireEvent.click(submitButton);
178+
await expect(mockCreateAccount).toHaveBeenCalledWith({
179+
scope: MultichainNetworks.SOLANA,
180+
entropySource: KEYRING_ID,
181+
accountNameSuggestion: 'Test Account',
182+
});
183+
});
141184

142-
await expect(mockCreateAccount).toHaveBeenCalledWith({
143-
scope: MultichainNetworks.SOLANA,
144-
entropySource: KEYRING_ID,
145-
accountNameSuggestion: 'Test Account',
185+
it('closes the modal when clicking "Not Now"', async () => {
186+
const notNowButton = screen.getByTestId('not-now-button');
187+
fireEvent.click(notNowButton);
188+
189+
await waitFor(() => {
190+
expect(mockOnClose).toHaveBeenCalled();
191+
});
192+
});
146193
});
147-
expect(mockOnClose).toHaveBeenCalled();
148-
});
149194

150-
it('closes the modal when clicking "Not Now"', () => {
151-
const notNowButton = screen.getByTestId('not-now-button');
152-
fireEvent.click(notNowButton);
153-
expect(mockOnClose).toHaveBeenCalled();
195+
describe('when the user has a Solana account', () => {
196+
beforeEach(() => {
197+
const store = configureStore({
198+
metamask: {
199+
...mockState.metamask,
200+
announcements: {
201+
[NOTIFICATION_SOLANA_ON_METAMASK]: {
202+
date: '2025-03-03',
203+
id: NOTIFICATION_SOLANA_ON_METAMASK,
204+
isShown: false,
205+
},
206+
},
207+
keyrings: [
208+
{
209+
metadata: {
210+
id: KEYRING_ID,
211+
},
212+
},
213+
],
214+
internalAccounts: {
215+
accounts: {
216+
[MOCK_ACCOUNT_SOLANA_MAINNET.id]: MOCK_ACCOUNT_SOLANA_MAINNET,
217+
},
218+
},
219+
},
220+
});
221+
renderWithProvider(<WhatsNewModal onClose={mockOnClose} />, store);
222+
});
223+
224+
it('renders Solana notification correctly', () => {
225+
expect(screen.getByTestId('solana-modal-body')).toBeInTheDocument();
226+
expect(
227+
screen.getByText(/Send, receive, and swap tokens/iu),
228+
).toBeInTheDocument();
229+
expect(
230+
screen.getByText(/Import Solana accounts/iu),
231+
).toBeInTheDocument();
232+
expect(
233+
screen.getByText(/More features coming soon/iu),
234+
).toBeInTheDocument();
235+
expect(screen.getByTestId('got-it-button')).toBeInTheDocument();
236+
expect(screen.getByTestId('not-now-button')).toBeInTheDocument();
237+
});
238+
239+
it('closes the modal when clicking "Got it"', async () => {
240+
const gotItButton = screen.getByTestId('got-it-button');
241+
fireEvent.click(gotItButton);
242+
243+
await waitFor(() => {
244+
expect(mockOnClose).toHaveBeenCalled();
245+
});
246+
});
247+
248+
it('closes the modal when clicking "Not Now"', async () => {
249+
const notNowButton = screen.getByTestId('not-now-button');
250+
fireEvent.click(notNowButton);
251+
252+
await waitFor(() => {
253+
expect(mockOnClose).toHaveBeenCalled();
254+
});
255+
});
256+
});
154257
});
155258
});
156259
});

0 commit comments

Comments
 (0)