Skip to content
2 changes: 1 addition & 1 deletion packages/snap/images/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 20 additions & 2 deletions packages/snap/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@
"confirmation.cancelButton": {
"message": "Cancel"
},
"confirmation.reviewAlertsButton": {
"message": "Review alert"
},
"confirmation.maliciousAck.title": {
"message": "Malicious request"
},
"confirmation.maliciousAck.description": {
"message": "If you confirm this request, you will probably lose your assets to a scammer."
},
"confirmation.maliciousAck.checkbox": {
"message": "I have acknowledged the risk and still want to proceed"
},
"confirmation.maliciousAck.proceed": {
"message": "Confirm"
},
"confirmation.maliciousAck.back": {
"message": "Go back"
},
"confirmation.closeButton": {
"message": "Close"
},
Expand Down Expand Up @@ -155,10 +173,10 @@
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
"message": "See details"
},
"confirmation.validationErrorSecurityAdviced": {
"message": "Security advice by"
"message": "Powered by"
},
"confirmation.transaction.accountmerge": {
"message": "Merge account"
Expand Down
22 changes: 20 additions & 2 deletions packages/snap/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@
"confirmation.cancelButton": {
"message": "Cancel"
},
"confirmation.reviewAlertsButton": {
"message": "Review alert"
},
"confirmation.maliciousAck.title": {
"message": "Malicious request"
},
"confirmation.maliciousAck.description": {
"message": "If you confirm this request, you will probably lose your assets to a scammer."
},
"confirmation.maliciousAck.checkbox": {
"message": "I have acknowledged the risk and still want to proceed"
},
"confirmation.maliciousAck.proceed": {
"message": "Confirm"
},
"confirmation.maliciousAck.back": {
"message": "Go back"
},
"confirmation.closeButton": {
"message": "Close"
},
Expand Down Expand Up @@ -155,10 +173,10 @@
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
"message": "See details"
},
"confirmation.validationErrorSecurityAdviced": {
"message": "Security advice by"
"message": "Powered by"
},
"confirmation.transaction.accountmerge": {
"message": "Merge account"
Expand Down
22 changes: 20 additions & 2 deletions packages/snap/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@
"confirmation.cancelButton": {
"message": "Cancel"
},
"confirmation.reviewAlertsButton": {
"message": "Review alert"
},
"confirmation.maliciousAck.title": {
"message": "Malicious request"
},
"confirmation.maliciousAck.description": {
"message": "If you confirm this request, you will probably lose your assets to a scammer."
},
"confirmation.maliciousAck.checkbox": {
"message": "I have acknowledged the risk and still want to proceed"
},
"confirmation.maliciousAck.proceed": {
"message": "Confirm"
},
"confirmation.maliciousAck.back": {
"message": "Go back"
},
"confirmation.closeButton": {
"message": "Close"
},
Expand Down Expand Up @@ -153,10 +171,10 @@
"message": "Security Alerts found potential risk. Only continue if you trust this site and every address involved."
},
"confirmation.validationErrorLearnMore": {
"message": "Learn more"
"message": "See details"
},
"confirmation.validationErrorSecurityAdviced": {
"message": "Security advice by"
"message": "Powered by"
},
"confirmation.transaction.accountmerge": {
"message": "Merge account"
Expand Down
2 changes: 1 addition & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/snap-stellar-wallet.git"
},
"source": {
"shasum": "YP/x/UxnvPyYy6yLPeQM2j1PZR8D8I4xUrgqoUnWbSE=",
"shasum": "0bXKXi1JKlz9Tl+t7sHnw2HInhPpNtkrTIrUdRD/n4I=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/snap/src/handlers/user-input/userInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createEventHandlers as createSignChangeTrustOptInEvents } from '../../u
import { createEventHandlers as createSignChangeTrustOptOutEvents } from '../../ui/confirmation/views/ConfirmSignChangeTrustOptOut/events';
import { createEventHandlers as createSignMessageEvents } from '../../ui/confirmation/views/ConfirmSignMessage/events';
import { createEventHandlers as createSignTransactionEvents } from '../../ui/confirmation/views/ConfirmSignTransaction/events';
import { createEventHandlers as createMaliciousAcknowledgementEvents } from '../../ui/confirmation/views/MaliciousAcknowledgement/events';
import {
withCatchAndThrowSnapError,
createPrefixedLogger,
Expand Down Expand Up @@ -51,6 +52,7 @@ export class UserInputHandler {
...createSignChangeTrustOptInEvents(),
...createSignChangeTrustOptOutEvents(),
...createConfirmSendTransactionEvents(),
...createMaliciousAcknowledgementEvents(),
};

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/snap/src/ui/confirmation/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,11 @@ export type ConfirmationBaseProps = Partial<ContextWithPrices> & {
networkImage: string | null;
origin: string;
feeData?: FeeData;
// Identifies the active view so shared event handlers (e.g. the malicious
// acknowledgement screen) can re-render the correct confirmation.
interfaceKey?: ConfirmationInterfaceKey;
// True while the malicious acknowledgement screen is shown over the confirmation.
acknowledgementScreen?: boolean;
// Whether the user has checked the "I acknowledge the risk" box on that screen.
acknowledged?: boolean;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { ComponentOrElement } from '@metamask/snaps-sdk';

import { ConfirmationFooter } from './ConfirmationFooter';
import { i18n } from '../../../utils';
import { MaliciousAcknowledgementFormNames } from '../views/MaliciousAcknowledgement/constants';

const translate = i18n('en');

type Element = {
type?: string;
props?: Record<string, unknown>;
};

/**
* Finds the first button element in the tree with the given name.
*
* @param node - The element to search.
* @param name - The button name to match.
* @returns The matching button props, or undefined.
*/
function findButton(
node: ComponentOrElement | null,
name: string,
): Record<string, unknown> | undefined {
if (typeof node !== 'object' || node === null) {
return undefined;
}
const element = node as Element;
if (element.type === 'Button' && element.props?.name === name) {
return element.props;
}
const children = element.props?.children;
const list = Array.isArray(children) ? children : [children];
for (const child of list) {
const found = findButton(child as ComponentOrElement | null, name);
if (found) {
return found;
}
}
return undefined;
}

describe('ConfirmationFooter', () => {
const baseProps = {
locale: 'en',
cancelButtonName: 'cancel',
confirmButtonName: 'confirm',
};

it('renders the confirm button when acknowledgement is not required', () => {
const footer = ConfirmationFooter({ ...baseProps });

const confirm = findButton(footer, 'confirm');
expect(confirm).toBeDefined();
expect(confirm?.children).toBe(translate('confirmation.confirmButton'));
expect(
findButton(footer, MaliciousAcknowledgementFormNames.Review),
).toBeUndefined();
});

it('disables the confirm button when confirmDisabled is true', () => {
const footer = ConfirmationFooter({ ...baseProps, confirmDisabled: true });

expect(findButton(footer, 'confirm')?.disabled).toBe(true);
});

it('renders the review-alerts button when acknowledgement is required', () => {
const footer = ConfirmationFooter({
...baseProps,
requiresAcknowledgement: true,
});

const review = findButton(footer, MaliciousAcknowledgementFormNames.Review);
expect(review).toBeDefined();
expect(review?.children).toBe(translate('confirmation.reviewAlertsButton'));
expect(findButton(footer, 'confirm')).toBeUndefined();
});

it('falls back to the disabled confirm button when blocked, even if acknowledgement is required', () => {
const footer = ConfirmationFooter({
...baseProps,
requiresAcknowledgement: true,
confirmDisabled: true,
});

expect(
findButton(footer, MaliciousAcknowledgementFormNames.Review),
).toBeUndefined();
expect(findButton(footer, 'confirm')?.disabled).toBe(true);
});

it('always renders the cancel button', () => {
const footer = ConfirmationFooter({
...baseProps,
requiresAcknowledgement: true,
});

expect(findButton(footer, 'cancel')?.children).toBe(
translate('confirmation.cancelButton'),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ComponentOrElement } from '@metamask/snaps-sdk';
import { Button, Footer } from '@metamask/snaps-sdk/jsx';

import type { Locale } from '../../../utils';
import { i18n } from '../../../utils';
import { MaliciousAcknowledgementFormNames } from '../views/MaliciousAcknowledgement/constants';

type ConfirmationFooterProps = {
locale: string;
cancelButtonName: string;
confirmButtonName: string;
confirmDisabled?: boolean;
// When true, the primary button becomes "Review alerts" and routes the user
// through the malicious acknowledgement screen instead of confirming directly.
requiresAcknowledgement?: boolean;
};

/**
* Shared confirmation footer (cancel + primary button).
*
* Centralizes the malicious-acknowledgement behavior: a malicious scan result
* swaps the primary button from "Confirm" to "Review alerts" rather than
* disabling it, so the user keeps a "proceed anyway" path behind friction.
*
* A blocking state (`confirmDisabled`, e.g. failed background re-validation)
* takes priority over the acknowledgement swap: we fall back to the disabled
* "Confirm" button so the user can never enter the acknowledgement flow for a
* transaction that is no longer valid.
*
* @param props - The footer props.
* @param props.locale - The active locale.
* @param props.cancelButtonName - Event name for the cancel button.
* @param props.confirmButtonName - Event name for the confirm button.
* @param props.confirmDisabled - Whether the confirm button is disabled.
* @param props.requiresAcknowledgement - Whether to show "Review alerts" instead of "Confirm".
* @returns The footer.
*/
export const ConfirmationFooter = ({
locale,
cancelButtonName,
confirmButtonName,
confirmDisabled = false,
requiresAcknowledgement = false,
}: ConfirmationFooterProps): ComponentOrElement => {
const t = i18n(locale as Locale);

return (
<Footer>
<Button name={cancelButtonName}>{t('confirmation.cancelButton')}</Button>
{requiresAcknowledgement && !confirmDisabled ? (
<Button name={MaliciousAcknowledgementFormNames.Review}>
{t('confirmation.reviewAlertsButton')}
</Button>
) : (
<Button name={confirmButtonName} disabled={confirmDisabled}>
{t('confirmation.confirmButton')}
</Button>
)}
</Footer>
);
};
1 change: 1 addition & 0 deletions packages/snap/src/ui/confirmation/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './Asset';
export * from './TransactionAlert';
export * from './TransactionValidationAlert';
export * from './ConfirmationAlerts';
export * from './ConfirmationFooter';
Loading
Loading