From 7a1fc95bdf8953c3ae2f1d6f199cf465a3d96e4c Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Fri, 7 Nov 2025 15:57:04 +0800 Subject: [PATCH 1/2] feat: show shield entry modal on settings --- shared/lib/shield.ts | 2 +- .../shield-entry-modal/shield-entry-modal.tsx | 13 +++++++++++- ui/pages/settings/settings.component.js | 20 ++++++++++++++++--- ui/pages/settings/settings.container.js | 2 ++ ui/selectors/subscription/subscription.ts | 12 ++++++++++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/shared/lib/shield.ts b/shared/lib/shield.ts index 6a4b9b9f2363..874e6c4703f6 100644 --- a/shared/lib/shield.ts +++ b/shared/lib/shield.ts @@ -8,7 +8,7 @@ import { DAY } from '../constants/time'; const SUBSCRIPTION_ENDING_SOON_DAYS = DAY; -function getShieldSubscription( +export function getShieldSubscription( subscriptions: Subscription | Subscription[], ): Subscription | undefined { let shieldSubscription: Subscription | undefined; diff --git a/ui/components/app/shield-entry-modal/shield-entry-modal.tsx b/ui/components/app/shield-entry-modal/shield-entry-modal.tsx index da9130766ca1..fbcffe73ba22 100644 --- a/ui/components/app/shield-entry-modal/shield-entry-modal.tsx +++ b/ui/components/app/shield-entry-modal/shield-entry-modal.tsx @@ -38,7 +38,14 @@ import { getShouldSubmitEventsForShieldEntryModal } from '../../../selectors'; // TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860 // eslint-disable-next-line @typescript-eslint/naming-convention -export default function ShieldEntryModal() { +export default function ShieldEntryModal({ + // Whether to skip event submission (e.g., when opened from a user action) + skipEventSubmission = false, + onClose, +}: { + skipEventSubmission?: boolean; + onClose?: () => void; +}) { const t = useI18nContext(); const dispatch = useDispatch(); const navigate = useNavigate(); @@ -47,6 +54,10 @@ export default function ShieldEntryModal() { ); const handleOnClose = () => { + if (skipEventSubmission) { + onClose?.(); + return; + } if (shouldSubmitEvent) { dispatch( submitSubscriptionUserEvents({ diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index f917c9b4d548..89fe5a864646 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -62,6 +62,7 @@ import { import { SnapIcon } from '../../components/app/snaps/snap-icon'; import { SnapSettingsRenderer } from '../../components/app/snaps/snap-settings-page'; import PasswordOutdatedModal from '../../components/app/password-outdated-modal'; +import ShieldEntryModal from '../../components/app/shield-entry-modal'; import SettingsTab from './settings-tab'; import AdvancedTab from './advanced-tab'; import InfoTab from './info-tab'; @@ -97,6 +98,7 @@ class SettingsPage extends PureComponent { backRoute: PropTypes.string, conversionDate: PropTypes.number, currentPath: PropTypes.string, + hasShieldSubscription: PropTypes.bool, isAddressEntryPage: PropTypes.bool, isMetaMaskShieldFeatureEnabled: PropTypes.bool, isPasswordChangePage: PropTypes.bool, @@ -122,6 +124,7 @@ class SettingsPage extends PureComponent { lastFetchedConversionDate: null, searchResults: [], searchText: '', + showShieldEntryModal: false, }; componentDidMount() { @@ -184,6 +187,12 @@ class SettingsPage extends PureComponent { }, )} > + {this.state.showShieldEntryModal && ( + this.setState({ showShieldEntryModal: false })} + /> + )} {isSeedlessPasswordOutdated && } + onSelect={(key) => { + if (key === TRANSACTION_SHIELD_ROUTE && !hasShieldSubscription) { + this.setState({ showShieldEntryModal: true }); + return; + } navigate(key, { state: { fromPage: currentPath }, - }) - } + }); + }} /> ); } diff --git a/ui/pages/settings/settings.container.js b/ui/pages/settings/settings.container.js index 8e457d50d391..1a44abcfab3e 100644 --- a/ui/pages/settings/settings.container.js +++ b/ui/pages/settings/settings.container.js @@ -49,6 +49,7 @@ import { getSnapName } from '../../helpers/utils/util'; import { decodeSnapIdFromPathname } from '../../helpers/utils/snaps'; import { getIsSeedlessPasswordOutdated } from '../../ducks/metamask/metamask'; import { getIsMetaMaskShieldFeatureEnabled } from '../../../shared/modules/environment'; +import { getHasShieldSubscription } from '../../selectors/subscription/subscription'; import Settings from './settings.component'; const ROUTES_TO_I18N_KEYS = { @@ -176,6 +177,7 @@ const mapStateToProps = (state, ownProps) => { backRoute, conversionDate, currentPath: pathname, + hasShieldSubscription: getHasShieldSubscription(state), isAddressEntryPage, isMetaMaskShieldFeatureEnabled: getIsMetaMaskShieldFeatureEnabled(), isPasswordChangePage, diff --git a/ui/selectors/subscription/subscription.ts b/ui/selectors/subscription/subscription.ts index fe5c91003f7a..b3fa245412be 100644 --- a/ui/selectors/subscription/subscription.ts +++ b/ui/selectors/subscription/subscription.ts @@ -6,7 +6,10 @@ import { Subscription, SubscriptionControllerState, } from '@metamask/subscription-controller'; -import { getIsShieldSubscriptionActive } from '../../../shared/lib/shield'; +import { + getIsShieldSubscriptionActive, + getShieldSubscription, +} from '../../../shared/lib/shield'; export type SubscriptionState = { metamask: SubscriptionControllerState & { @@ -49,3 +52,10 @@ export function getLastUsedShieldSubscriptionPaymentDetails( ): CachedLastSelectedPaymentMethod | undefined { return state.metamask.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD]; } + +export function getHasShieldSubscription(state: SubscriptionState): boolean { + const shieldSubscription = getShieldSubscription( + state.metamask.subscriptions, + ); + return Boolean(shieldSubscription); +} From 2e5fc949c9212d4dbb3fc5f839ab73cd266a7fef Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Mon, 10 Nov 2025 14:17:53 +0800 Subject: [PATCH 2/2] feat: show modal only if user has not subscribed to shield --- shared/lib/shield.ts | 2 +- ui/pages/settings/settings.component.js | 6 +++--- ui/pages/settings/settings.container.js | 4 ++-- ui/selectors/subscription/subscription.ts | 13 ++++--------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/shared/lib/shield.ts b/shared/lib/shield.ts index 874e6c4703f6..6a4b9b9f2363 100644 --- a/shared/lib/shield.ts +++ b/shared/lib/shield.ts @@ -8,7 +8,7 @@ import { DAY } from '../constants/time'; const SUBSCRIPTION_ENDING_SOON_DAYS = DAY; -export function getShieldSubscription( +function getShieldSubscription( subscriptions: Subscription | Subscription[], ): Subscription | undefined { let shieldSubscription: Subscription | undefined; diff --git a/ui/pages/settings/settings.component.js b/ui/pages/settings/settings.component.js index 89fe5a864646..ad288da9eba4 100644 --- a/ui/pages/settings/settings.component.js +++ b/ui/pages/settings/settings.component.js @@ -98,7 +98,7 @@ class SettingsPage extends PureComponent { backRoute: PropTypes.string, conversionDate: PropTypes.number, currentPath: PropTypes.string, - hasShieldSubscription: PropTypes.bool, + hasSubscribedToShield: PropTypes.bool, isAddressEntryPage: PropTypes.bool, isMetaMaskShieldFeatureEnabled: PropTypes.bool, isPasswordChangePage: PropTypes.bool, @@ -386,7 +386,7 @@ class SettingsPage extends PureComponent { useExternalServices, settingsPageSnaps, isMetaMaskShieldFeatureEnabled, - hasShieldSubscription, + hasSubscribedToShield, } = this.props; const { t } = this.context; @@ -483,7 +483,7 @@ class SettingsPage extends PureComponent { return matchPath(key, currentPath); }} onSelect={(key) => { - if (key === TRANSACTION_SHIELD_ROUTE && !hasShieldSubscription) { + if (key === TRANSACTION_SHIELD_ROUTE && !hasSubscribedToShield) { this.setState({ showShieldEntryModal: true }); return; } diff --git a/ui/pages/settings/settings.container.js b/ui/pages/settings/settings.container.js index 1a44abcfab3e..00290ec69960 100644 --- a/ui/pages/settings/settings.container.js +++ b/ui/pages/settings/settings.container.js @@ -49,7 +49,7 @@ import { getSnapName } from '../../helpers/utils/util'; import { decodeSnapIdFromPathname } from '../../helpers/utils/snaps'; import { getIsSeedlessPasswordOutdated } from '../../ducks/metamask/metamask'; import { getIsMetaMaskShieldFeatureEnabled } from '../../../shared/modules/environment'; -import { getHasShieldSubscription } from '../../selectors/subscription/subscription'; +import { getHasSubscribedToShield } from '../../selectors/subscription/subscription'; import Settings from './settings.component'; const ROUTES_TO_I18N_KEYS = { @@ -177,7 +177,7 @@ const mapStateToProps = (state, ownProps) => { backRoute, conversionDate, currentPath: pathname, - hasShieldSubscription: getHasShieldSubscription(state), + hasSubscribedToShield: getHasSubscribedToShield(state), isAddressEntryPage, isMetaMaskShieldFeatureEnabled: getIsMetaMaskShieldFeatureEnabled(), isPasswordChangePage, diff --git a/ui/selectors/subscription/subscription.ts b/ui/selectors/subscription/subscription.ts index b3fa245412be..1b6a67aaaab9 100644 --- a/ui/selectors/subscription/subscription.ts +++ b/ui/selectors/subscription/subscription.ts @@ -6,10 +6,7 @@ import { Subscription, SubscriptionControllerState, } from '@metamask/subscription-controller'; -import { - getIsShieldSubscriptionActive, - getShieldSubscription, -} from '../../../shared/lib/shield'; +import { getIsShieldSubscriptionActive } from '../../../shared/lib/shield'; export type SubscriptionState = { metamask: SubscriptionControllerState & { @@ -53,9 +50,7 @@ export function getLastUsedShieldSubscriptionPaymentDetails( return state.metamask.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD]; } -export function getHasShieldSubscription(state: SubscriptionState): boolean { - const shieldSubscription = getShieldSubscription( - state.metamask.subscriptions, - ); - return Boolean(shieldSubscription); +export function getHasSubscribedToShield(state: SubscriptionState): boolean { + const hasSubscribedToShield = state.metamask.customerId; + return Boolean(hasSubscribedToShield); }