Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9018409
feat(shared): add MetaMetricsEventAccountType.Snap
ccharly Feb 15, 2024
7f20fa1
feat(ui): forward trackEvent context to confirmation templates
ccharly Feb 15, 2024
a9ea8dd
feat(metametrics): add event for snap account page selection
ccharly Feb 15, 2024
fa364f1
feat(metametrics): add events for snap account confirmation/creation/…
ccharly Feb 15, 2024
d95e080
docs: fix confirmation template folder path
ccharly Feb 15, 2024
84cd1ed
feat(confirmation): add onLoad hook
ccharly Feb 21, 2024
78cb7dc
refactor(ui): add specialized events for view/confirm/cancel snap acc…
ccharly Feb 21, 2024
8f711cd
feat(snap-keyring): add trackEvent helper
ccharly Feb 21, 2024
537fea4
feat(snap-keyring): track success/error view/confirmation events
ccharly Feb 21, 2024
16b81c3
feat(snap-keyring): track account creation/failure event
ccharly Feb 21, 2024
df46423
refactor(confirmation): improve hook naming for snap account redirect
ccharly Feb 21, 2024
9685047
feat(metametrics): add events for snap account transactions
ccharly Feb 21, 2024
0e79f6b
feat(metametrics): add events during snap account transaction loading…
ccharly Feb 21, 2024
401d1bd
feat(metametrics): add events during snap account removal
ccharly Feb 22, 2024
296e779
fix(metametrics): update snap accounts wording: Cancelled -> Canceled
ccharly Mar 6, 2024
c33324f
refactor(metametrics): remove unused AddSnapAccountClicked
ccharly Mar 6, 2024
b83cdbc
fix(snap-keyring): remove extraneous "canceled" events
ccharly Mar 6, 2024
19bba6e
fix(snap-keyring): fix add/remove snap account event flow
ccharly Mar 6, 2024
27723df
refactor(snap-keyring): add some TODOs for dialog/metametrics
ccharly Mar 7, 2024
1ab98e8
Merge branch 'develop' into feature/247/more-snap-account-metrics
ccharly Mar 7, 2024
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
87 changes: 83 additions & 4 deletions app/scripts/lib/snap-keyring/snap-keyring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { SnapKeyring } from '@metamask/eth-snap-keyring';
import type { SnapController } from '@metamask/snaps-controllers';
import browser from 'webextension-polyfill';
import { SnapId } from '@metamask/snaps-sdk';
import {
MetaMetricsEventCategory,
MetaMetricsEventName,
MetaMetricsEventAccountType,
} from '../../../../shared/constants/metametrics';
import { SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES } from '../../../../shared/constants/app';
import { t } from '../../translate';
import MetamaskController from '../../metamask-controller';
Expand Down Expand Up @@ -34,6 +39,7 @@ export const getAccountsBySnapId = async (
* @param persistKeyringHelper - A function that persists all keyrings in the vault.
* @param setSelectedAccountHelper - A function to update current selected account.
* @param removeAccountHelper - A function to help remove an account based on its address.
* @param trackEvent - A function to track MetaMetrics events.
* @returns The constructed SnapKeyring builder instance with the following methods:
* - `saveState`: Persists all keyrings in the keyring controller.
* - `addAccount`: Initiates the process of adding an account with user confirmation and handling the user input.
Expand All @@ -45,6 +51,10 @@ export const snapKeyringBuilder = (
persistKeyringHelper: () => Promise<void>,
setSelectedAccountHelper: (address: string) => void,
removeAccountHelper: (address: string) => Promise<any>,
trackEvent: (
payload: Record<string, any>,
options?: Record<string, any>,
) => void,
) => {
const builder = (() => {
return new SnapKeyring(getSnapController() as any, {
Expand Down Expand Up @@ -101,15 +111,30 @@ export const snapKeyringBuilder = (
snapId: string,
handleUserInput: (accepted: boolean) => Promise<void>,
) => {
const snapName = getSnapName(controllerMessenger, snapId);
const { id: addAccountApprovalId } = controllerMessenger.call(
'ApprovalController:startFlow',
);

const trackSnapAccountEvent = (event: MetaMetricsEventName) => {
trackEvent({
event,
category: MetaMetricsEventCategory.Accounts,
properties: {
account_type: MetaMetricsEventAccountType.Snap,
snap_id: snapId,
snap_name: snapName,
},
});
};

const learnMoreLink =
'https://support.metamask.io/hc/en-us/articles/360015289452-How-to-add-accounts-in-your-wallet';

// Since we use this in the finally, better to give it a default value if the controller call fails
let confirmationResult = false;
try {
const confirmationResult = Boolean(
confirmationResult = Boolean(
await controllerMessenger.call(
'ApprovalController:addRequest',
{
Expand Down Expand Up @@ -139,6 +164,12 @@ export const snapKeyringBuilder = (
internalAccount.id,
);

// TODO: Add events tracking to the dialog itself, so that events are more
// "linked" to UI actions
// User should now see the "Successfuly added account" page
trackSnapAccountEvent(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for the reviewers:

I thought of maybe augmenting the showSuccess function (or event deeper, to the component itself) to support those events, like having a:

events: {
  onLoad: MetaMetricsEventName.AddSnapAccountSuccessViewed,
  onSubmit: MetaMetricsEventName.AddSnapAccountSuccessClicked
},

So that our component would take care "hooking" those actions and fire events automatically. But this requires a bit more effort to implement properly.

I'm also open for suggestion here, since having those metrics in the snap-keyring.ts feels a bit wrong to me.

WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

I really like this idea and I agree that the implementation would scope creep this task a bit. If you strongly feel that it should be done first then I recommend opening a new PR for that change

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since I'd like to move on with this PR, I'll probably keep it that way for now, and I'll leave a TODO to improve that later on.

MetaMetricsEventName.AddSnapAccountSuccessViewed,
);
await showSuccess(
controllerMessenger,
snapId,
Expand All @@ -152,10 +183,14 @@ export const snapKeyringBuilder = (
learnMoreLink,
},
);

// User has clicked on "OK"
trackSnapAccountEvent(
MetaMetricsEventName.AddSnapAccountSuccessClicked,
);
} catch (e) {
const error = (e as Error).message;

const snapName = getSnapName(controllerMessenger, snapId);
await showError(
controllerMessenger,
snapId,
Expand All @@ -173,15 +208,25 @@ export const snapKeyringBuilder = (
},
);

trackSnapAccountEvent(MetaMetricsEventName.AccountAddFailed);

throw new Error(
`Error occurred while creating snap account: ${error}`,
);
}
} else {
// User has cancelled account creation
await handleUserInput(confirmationResult);

throw new Error('User denied account creation');
}
} finally {
// We do not have a `else` clause here, as it's used if the request was
// canceled by the user, thus it's not a "fail" (not an error).
if (confirmationResult) {
trackSnapAccountEvent(MetaMetricsEventName.AccountAdded);
}

controllerMessenger.call('ApprovalController:endFlow', {
id: addAccountApprovalId,
});
Expand All @@ -192,15 +237,30 @@ export const snapKeyringBuilder = (
snapId: string,
handleUserInput: (accepted: boolean) => Promise<void>,
) => {
const snapName = getSnapName(controllerMessenger, snapId);
const { id: removeAccountApprovalId } = controllerMessenger.call(
'ApprovalController:startFlow',
);

const learnMoreLink =
'https://support.metamask.io/hc/en-us/articles/360057435092-How-to-remove-an-account-from-your-MetaMask-wallet';

const trackSnapAccountEvent = (event: MetaMetricsEventName) => {
trackEvent({
event,
category: MetaMetricsEventCategory.Accounts,
properties: {
account_type: MetaMetricsEventAccountType.Snap,
snap_id: snapId,
snap_name: snapName,
},
});
};

// Since we use this in the finally, better to give it a default value if the controller call fails
let confirmationResult = false;
try {
const confirmationResult = Boolean(
confirmationResult = Boolean(
await controllerMessenger.call(
'ApprovalController:addRequest',
{
Expand All @@ -218,6 +278,12 @@ export const snapKeyringBuilder = (
await handleUserInput(confirmationResult);
await persistKeyringHelper();

// TODO: Add events tracking to the dialog itself, so that events are more
// "linked" to UI actions
// User should now see the "Successfuly removed account" page
trackSnapAccountEvent(
MetaMetricsEventName.RemoveSnapAccountSuccessViewed,
);
// This isn't actually an error, but we show it as one for styling reasons
await showError(
controllerMessenger,
Expand All @@ -231,10 +297,14 @@ export const snapKeyringBuilder = (
learnMoreLink,
},
);

// User has clicked on "OK"
trackSnapAccountEvent(
MetaMetricsEventName.RemoveSnapAccountSuccessClicked,
);
} catch (e) {
const error = (e as Error).message;

const snapName = getSnapName(controllerMessenger, snapId);
await showError(
controllerMessenger,
snapId,
Expand All @@ -252,15 +322,24 @@ export const snapKeyringBuilder = (
},
);

trackSnapAccountEvent(MetaMetricsEventName.AccountRemoveFailed);

throw new Error(
`Error occurred while removing snap account: ${error}`,
);
}
} else {
await handleUserInput(confirmationResult);

throw new Error('User denied account removal');
}
} finally {
// We do not have a `else` clause here, as it's used if the request was
// canceled by the user, thus it's not a "fail" (not an error).
if (confirmationResult) {
trackSnapAccountEvent(MetaMetricsEventName.AccountRemoved);
}

controllerMessenger.call('ApprovalController:endFlow', {
id: removeAccountApprovalId,
});
Expand Down
1 change: 1 addition & 0 deletions app/scripts/metamask-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,7 @@ export default class MetamaskController extends EventEmitter {
persistAndUpdateAccounts,
(address) => this.preferencesController.setSelectedAddress(address),
(address) => this.removeAccount(address),
this.metaMetricsController.trackEvent.bind(this.metaMetricsController),
),
);

Expand Down
2 changes: 1 addition & 1 deletion docs/confirmations.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ The `ConfirmationPage` component is already configured to display any approval r

In order to configure how the resulting confirmation is rendered, an **Approval Template** is required.

Create a new JavaScript file in `ui/pages/confirmation/templates` with the name matching the `type` used in the background approval request.
Create a new JavaScript file in `ui/pages/confirmations/confirmation/templates` with the name matching the `type` used in the background approval request.

### 2. Update Approval Templates

Expand Down
19 changes: 19 additions & 0 deletions shared/constants/metametrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ export enum MetaMetricsEventName {
AccountDetailMenuOpened = 'Account Details Menu Opened',
BlockExplorerLinkClicked = 'Block Explorer Clicked',
AccountRemoved = 'Account Removed',
AccountRemoveFailed = 'Account Remove Failed',
TestNetworksDisplayed = 'Test Networks Displayed',
AddNetworkButtonClick = 'Add Network Button Clicked',
CustomNetworkAdded = 'Custom Network Added',
Expand Down Expand Up @@ -676,13 +677,31 @@ export enum MetaMetricsEventName {
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
AddSnapAccountEnabled = 'Add Snap Account Enabled',
AddSnapAccountViewed = 'Add Snap Account Viewed',
AddSnapAccountConfirmed = 'Add Snap Account Confirmed',
AddSnapAccountCanceled = 'Add Snap Account Canceled',
AddSnapAccountSuccessViewed = 'Add Snap Account Success Viewed',
AddSnapAccountSuccessClicked = 'Add Snap Account Success Clicked',
RemoveSnapAccountViewed = 'Remove Snap Account Viewed',
RemoveSnapAccountConfirmed = 'Remove Snap Account Confirmed',
RemoveSnapAccountCanceled = 'Remove Snap Account Canceled',
RemoveSnapAccountSuccessViewed = 'Remove Snap Account Success Viewed',
RemoveSnapAccountSuccessClicked = 'Remove Snap Account Success Clicked',
SnapAccountTransactionLoadingViewed = 'Snap Account Transaction Loading Viewed',
SnapAccountTransactionFinalizeViewed = 'Snap Account Transaction Finalize Viewed',
SnapAccountTransactionFinalizeRedirectGoToSiteClicked = 'Snap Account Transaction Finalize Redirect "Go To Site" Clicked',
SnapAccountTransactionFinalizeRedirectSnapUrlClicked = 'Snap Account Transaction Finalize Redirect "Snap URL" Clicked',
SnapAccountTransactionFinalizeClosed = 'Snap Account Transaction Finalize Closed',
///: END:ONLY_INCLUDE_IF
}

export enum MetaMetricsEventAccountType {
Default = 'metamask',
Hardware = 'hardware',
Imported = 'imported',
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
Snap = 'snap',
///: END:ONLY_INCLUDE_IF
}

export enum MetaMetricsEventAccountImportType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,14 @@ export const AccountListMenu = ({
startIconName={IconName.Snaps}
onClick={() => {
onClose();
trackEvent({
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AccountAddSelected,
properties: {
account_type: MetaMetricsEventAccountType.Snap,
location: 'Main Menu',
},
});
global.platform.openTab({
url: process.env.ACCOUNT_SNAPS_DIRECTORY_URL,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { ConfirmGasDisplay } from '../components/confirm-gas-display';
import updateTxData from '../../../../shared/modules/updateTxData';
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
import { KeyringType } from '../../../../shared/constants/keyring';
import SnapAccountTransactionLoadingScreen from '../../snap-account-transaction-loading-screen/snap-account-transaction-loading-screen';
///: END:ONLY_INCLUDE_IF
import { isHardwareKeyring } from '../../../helpers/utils/hardware';
import FeeDetailsComponent from '../components/fee-details-component/fee-details-component';
Expand Down Expand Up @@ -102,6 +103,9 @@ export default class ConfirmTransactionBase extends Component {
unapprovedTxCount: PropTypes.number,
customGas: PropTypes.object,
addToAddressBookIfNew: PropTypes.func,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
fromInternalAccount: PropTypes.object,
///: END:ONLY_INCLUDE_IF
keyringForAccount: PropTypes.object,
// Component props
actionKey: PropTypes.string,
Expand Down Expand Up @@ -671,14 +675,21 @@ export default class ConfirmTransactionBase extends Component {
toAccounts,
toAddress,
keyringForAccount,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
fromInternalAccount,
///: END:ONLY_INCLUDE_IF
} = this.props;

let loadingIndicatorMessage;

switch (keyringForAccount?.type) {
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
case KeyringType.snap:
loadingIndicatorMessage = this.context.t('loadingScreenSnapMessage');
loadingIndicatorMessage = (
<SnapAccountTransactionLoadingScreen
internalAccount={fromInternalAccount}
></SnapAccountTransactionLoadingScreen>
);
break;
///: END:ONLY_INCLUDE_IF
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ const mapStateToProps = (state, ownProps) => {
const tokenToAddress = getTokenAddressParam(transactionData);

const { balance } = accounts[fromAddress];
const fromName = getInternalAccountByAddress(state, fromAddress)?.metadata
.name;
const fromInternalAccount = getInternalAccountByAddress(state, fromAddress);
const fromName = fromInternalAccount?.metadata.name;
const keyring = findKeyringForAddress(state, fromAddress);

const isSendingAmount =
Expand Down Expand Up @@ -319,6 +319,9 @@ const mapStateToProps = (state, ownProps) => {
isBuyableChain,
useCurrencyRateCheck: getUseCurrencyRateCheck(state),
keyringForAccount: keyring,
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
fromInternalAccount,
///: END:ONLY_INCLUDE_IF
isUsingPaymaster,
isSigningOrSubmitting,
isUserOpContractDeployError,
Expand Down
13 changes: 13 additions & 0 deletions ui/pages/confirmations/confirmation/confirmation.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React, {
useMemo,
useReducer,
useState,
useContext,
} from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
Expand All @@ -21,6 +22,7 @@ import ConfirmationWarningModal from '../components/confirmation-warning-modal';
import { DEFAULT_ROUTE } from '../../../helpers/constants/routes';
import { Size, TextColor } from '../../../helpers/constants/design-system';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { MetaMetricsContext } from '../../../contexts/metametrics';
import {
///: BEGIN:ONLY_INCLUDE_IF(snaps)
getTargetSubjectMetadata,
Expand Down Expand Up @@ -195,6 +197,7 @@ export default function ConfirmationPage({
redirectToHomeOnZeroConfirmations = true,
}) {
const t = useI18nContext();
const trackEvent = useContext(MetaMetricsContext);
const dispatch = useDispatch();
const history = useHistory();
const pendingConfirmations = useSelector(
Expand Down Expand Up @@ -312,6 +315,9 @@ export default function ConfirmationPage({
matchedChain,
currencySymbolWarning,
},
// Passing `t` in the contexts object is a bit redundant but since it's a
// context too, it makes sense (for completeness)
{ t, trackEvent },
)
: {};
}, [
Expand All @@ -321,12 +327,19 @@ export default function ConfirmationPage({
history,
matchedChain,
currencySymbolWarning,
trackEvent,
///: BEGIN:ONLY_INCLUDE_IF(snaps,keyring-snaps)
isSnapDialog,
snapName,
///: END:ONLY_INCLUDE_IF
]);

useEffect(() => {
if (templatedValues.onLoad) {
templatedValues.onLoad();
}
}, [templatedValues]);

useEffect(() => {
// If the number of pending confirmations reduces to zero when the user
// return them to the default route. Otherwise, if the number of pending
Expand Down
Loading