From 90531515b1b94bcfe7d70113064392f31769314c Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 29 Jan 2026 10:15:34 +0700 Subject: [PATCH 1/6] feat: add clearLastSelectedPaymentMethod --- .../src/SubscriptionController.test.ts | 70 +++++++++++++++++++ .../src/SubscriptionController.ts | 22 +++++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index 2377584f8b9..e8ea8984334 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -1762,6 +1762,76 @@ describe('SubscriptionController', () => { }); }); + describe('clearLastSelectedPaymentMethod', () => { + it('should clear last selected payment method successfully', async () => { + await withController( + { + state: { + lastSelectedPaymentMethod: { + [PRODUCT_TYPES.SHIELD]: { + type: PAYMENT_TYPES.byCard, + plan: RECURRING_INTERVALS.month, + }, + }, + }, + }, + async ({ controller }) => { + expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ + [PRODUCT_TYPES.SHIELD]: { + type: PAYMENT_TYPES.byCard, + plan: RECURRING_INTERVALS.month, + }, + }); + + controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + + expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ + [PRODUCT_TYPES.SHIELD]: null, + }); + }, + ); + }); + + it('should do nothing when lastSelectedPaymentMethod is undefined', async () => { + await withController(async ({ controller }) => { + expect(controller.state.lastSelectedPaymentMethod).toBeUndefined(); + + controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + + expect(controller.state.lastSelectedPaymentMethod).toBeUndefined(); + }); + }); + + it('should set the product to null while preserving the state object', async () => { + await withController( + { + state: { + lastSelectedPaymentMethod: { + [PRODUCT_TYPES.SHIELD]: { + type: PAYMENT_TYPES.byCrypto, + paymentTokenAddress: '0x123', + paymentTokenSymbol: 'USDT', + plan: RECURRING_INTERVALS.month, + }, + }, + }, + }, + async ({ controller }) => { + expect( + controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], + ).not.toBeNull(); + + controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); + + expect(controller.state.lastSelectedPaymentMethod).toBeDefined(); + expect( + controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], + ).toBeNull(); + }, + ); + }); + }); + describe('submitSponsorshipIntents', () => { const MOCK_SUBMISSION_INTENTS_REQUEST: SubmitSponsorshipIntentsMethodParams = { diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index 43c8e17ec70..bd4363cbcf2 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -64,7 +64,7 @@ export type SubscriptionControllerState = { */ lastSelectedPaymentMethod?: Record< ProductType, - CachedLastSelectedPaymentMethod + CachedLastSelectedPaymentMethod | null >; }; @@ -687,7 +687,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< * Cache the last selected payment method for a specific product. * * @param product - The product to cache the payment method for. - * @param paymentMethod - The payment method to cache. + * @param paymentMethod - The payment method to cache, or undefined/null to clear. * @param paymentMethod.type - The type of the payment method. * @param paymentMethod.paymentTokenAddress - The payment token address. * @param paymentMethod.plan - The plan of the payment method. @@ -714,6 +714,22 @@ export class SubscriptionController extends StaticIntervalPollingController()< }); } + /** + * Clear the last selected payment method for a specific product. + * + * @param product - The product to clear the payment method for. + */ + clearLastSelectedPaymentMethod(product: ProductType): void { + this.update((state) => { + if (state.lastSelectedPaymentMethod) { + state.lastSelectedPaymentMethod = { + ...state.lastSelectedPaymentMethod, + [product]: null, + }; + } + }); + } + /** * Submit sponsorship intents to the Subscription Service backend. * @@ -991,7 +1007,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< * @throws an error if the value is not a valid crypto payment method. */ #assertIsPaymentMethodCrypto( - value: CachedLastSelectedPaymentMethod | undefined, + value: CachedLastSelectedPaymentMethod | null | undefined, ): asserts value is Required { if ( !value || From 9c68b732baa946215bb8601de16e3c7887357b13 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 29 Jan 2026 10:18:12 +0700 Subject: [PATCH 2/6] chore: update changelog --- packages/subscription-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/subscription-controller/CHANGELOG.md b/packages/subscription-controller/CHANGELOG.md index bb8fee31064..02cdecc0b9e 100644 --- a/packages/subscription-controller/CHANGELOG.md +++ b/packages/subscription-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added SubscriptionController `clearLastSelectedPaymentMethod` method and udpate `lastSelectedPaymentMethod` property to have nullable value ([#7768](https://github.com/MetaMask/core/pull/7768)) + ### Changed - Bump `@metamask/transaction-controller` from `^62.9.2` to `^62.11.0` ([#7737](https://github.com/MetaMask/core/pull/7737), [#7760](https://github.com/MetaMask/core/pull/7760)) From a8c7083975366b868ca8658489f7170b527472a6 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 29 Jan 2026 14:47:49 +0700 Subject: [PATCH 3/6] feat: refactor to reassign instead of set null --- .../src/SubscriptionController.test.ts | 10 ++++------ .../src/SubscriptionController.ts | 11 +++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index e8ea8984334..0dca173933c 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -1785,9 +1785,7 @@ describe('SubscriptionController', () => { controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); - expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({ - [PRODUCT_TYPES.SHIELD]: null, - }); + expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({}); }, ); }); @@ -1802,7 +1800,7 @@ describe('SubscriptionController', () => { }); }); - it('should set the product to null while preserving the state object', async () => { + it('should remove the product key while preserving the state object', async () => { await withController( { state: { @@ -1819,14 +1817,14 @@ describe('SubscriptionController', () => { async ({ controller }) => { expect( controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], - ).not.toBeNull(); + ).toBeDefined(); controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); expect(controller.state.lastSelectedPaymentMethod).toBeDefined(); expect( controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], - ).toBeNull(); + ).toBeUndefined(); }, ); }); diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index bd4363cbcf2..422c1898623 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -64,7 +64,7 @@ export type SubscriptionControllerState = { */ lastSelectedPaymentMethod?: Record< ProductType, - CachedLastSelectedPaymentMethod | null + CachedLastSelectedPaymentMethod >; }; @@ -722,10 +722,9 @@ export class SubscriptionController extends StaticIntervalPollingController()< clearLastSelectedPaymentMethod(product: ProductType): void { this.update((state) => { if (state.lastSelectedPaymentMethod) { - state.lastSelectedPaymentMethod = { - ...state.lastSelectedPaymentMethod, - [product]: null, - }; + const { [product]: _, ...rest } = state.lastSelectedPaymentMethod; + state.lastSelectedPaymentMethod = + rest as typeof state.lastSelectedPaymentMethod; } }); } @@ -1007,7 +1006,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< * @throws an error if the value is not a valid crypto payment method. */ #assertIsPaymentMethodCrypto( - value: CachedLastSelectedPaymentMethod | null | undefined, + value: CachedLastSelectedPaymentMethod | undefined, ): asserts value is Required { if ( !value || From f697aa602f692d2188c1cda0ae591a2e5dba37a8 Mon Sep 17 00:00:00 2001 From: Tuna Date: Thu, 29 Jan 2026 14:49:22 +0700 Subject: [PATCH 4/6] fix: correct comment --- packages/subscription-controller/CHANGELOG.md | 2 +- packages/subscription-controller/src/SubscriptionController.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subscription-controller/CHANGELOG.md b/packages/subscription-controller/CHANGELOG.md index 02cdecc0b9e..9d7f18371dd 100644 --- a/packages/subscription-controller/CHANGELOG.md +++ b/packages/subscription-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added SubscriptionController `clearLastSelectedPaymentMethod` method and udpate `lastSelectedPaymentMethod` property to have nullable value ([#7768](https://github.com/MetaMask/core/pull/7768)) +- Added SubscriptionController `clearLastSelectedPaymentMethod` method ([#7768](https://github.com/MetaMask/core/pull/7768)) ### Changed diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index 422c1898623..b376bedef5b 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -687,7 +687,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< * Cache the last selected payment method for a specific product. * * @param product - The product to cache the payment method for. - * @param paymentMethod - The payment method to cache, or undefined/null to clear. + * @param paymentMethod - The payment method to cache. * @param paymentMethod.type - The type of the payment method. * @param paymentMethod.paymentTokenAddress - The payment token address. * @param paymentMethod.plan - The plan of the payment method. From 67ed40e91966552a613b492d9ce326fdd2c18193 Mon Sep 17 00:00:00 2001 From: Tuna Date: Fri, 30 Jan 2026 14:47:05 +0700 Subject: [PATCH 5/6] feat: update test case --- .../src/SubscriptionController.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index 0dca173933c..ef11fb3636c 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -1811,7 +1811,10 @@ describe('SubscriptionController', () => { paymentTokenSymbol: 'USDT', plan: RECURRING_INTERVALS.month, }, - }, + 'test-product-type': { + type: PAYMENT_TYPES.byCard, + }, + } as Record, }, }, async ({ controller }) => { @@ -1821,7 +1824,11 @@ describe('SubscriptionController', () => { controller.clearLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD); - expect(controller.state.lastSelectedPaymentMethod).toBeDefined(); + expect( + controller.state.lastSelectedPaymentMethod?.[ + 'test-product-type' as ProductType + ], + ).toBeDefined(); expect( controller.state.lastSelectedPaymentMethod?.[PRODUCT_TYPES.SHIELD], ).toBeUndefined(); From a6e2d2538245eac64f6af4ec6bcd628b6f22f90a Mon Sep 17 00:00:00 2001 From: Tuna Date: Fri, 30 Jan 2026 15:00:51 +0700 Subject: [PATCH 6/6] feat: register actions --- .../src/SubscriptionController.ts | 24 ++++++++++++++++++- packages/subscription-controller/src/index.ts | 2 ++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index b376bedef5b..2801b04d364 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -111,6 +111,16 @@ export type SubscriptionControllerSubmitSponsorshipIntentsAction = { handler: SubscriptionController['submitSponsorshipIntents']; }; +export type SubscriptionControllerCacheLastSelectedPaymentMethodAction = { + type: `${typeof controllerName}:cacheLastSelectedPaymentMethod`; + handler: SubscriptionController['cacheLastSelectedPaymentMethod']; +}; + +export type SubscriptionControllerClearLastSelectedPaymentMethodAction = { + type: `${typeof controllerName}:clearLastSelectedPaymentMethod`; + handler: SubscriptionController['clearLastSelectedPaymentMethod']; +}; + export type SubscriptionControllerLinkRewardsAction = { type: `${typeof controllerName}:linkRewards`; handler: SubscriptionController['linkRewards']; @@ -139,7 +149,9 @@ export type SubscriptionControllerActions = | SubscriptionControllerGetBillingPortalUrlAction | SubscriptionControllerSubmitSponsorshipIntentsAction | SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction - | SubscriptionControllerLinkRewardsAction; + | SubscriptionControllerLinkRewardsAction + | SubscriptionControllerCacheLastSelectedPaymentMethodAction + | SubscriptionControllerClearLastSelectedPaymentMethodAction; export type AllowedActions = | AuthenticationController.AuthenticationControllerGetBearerToken @@ -353,6 +365,16 @@ export class SubscriptionController extends StaticIntervalPollingController()< `${controllerName}:linkRewards`, this.linkRewards.bind(this), ); + + this.messenger.registerActionHandler( + `${controllerName}:cacheLastSelectedPaymentMethod`, + this.cacheLastSelectedPaymentMethod.bind(this), + ); + + this.messenger.registerActionHandler( + `${controllerName}:clearLastSelectedPaymentMethod`, + this.clearLastSelectedPaymentMethod.bind(this), + ); } /** diff --git a/packages/subscription-controller/src/index.ts b/packages/subscription-controller/src/index.ts index feea002d74b..f219651b544 100644 --- a/packages/subscription-controller/src/index.ts +++ b/packages/subscription-controller/src/index.ts @@ -16,6 +16,8 @@ export type { SubscriptionControllerOptions, SubscriptionControllerStateChangeEvent, SubscriptionControllerSubmitSponsorshipIntentsAction, + SubscriptionControllerCacheLastSelectedPaymentMethodAction, + SubscriptionControllerClearLastSelectedPaymentMethodAction, SubscriptionControllerLinkRewardsAction, SubscriptionControllerSubmitShieldSubscriptionCryptoApprovalAction, AllowedActions,