From e24c6c16d44ea4215687f2844f896b6b4daf8fe0 Mon Sep 17 00:00:00 2001 From: Tuna Date: Tue, 11 Nov 2025 16:39:10 +0700 Subject: [PATCH 1/4] feat: add last subscription response --- .../src/SubscriptionController.test.ts | 25 ++++++++++++ .../src/SubscriptionController.ts | 38 ++++++++++++++++--- .../src/SubscriptionService.test.ts | 1 + packages/subscription-controller/src/types.ts | 8 ++++ 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index 6b91f7218c8..ea9bfe1f041 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -80,6 +80,7 @@ const MOCK_SUBSCRIPTION: Subscription = { last4: '1234', }, }, + isEligibleForSupport: true, }; const MOCK_PRODUCT_PRICE: ProductPricing = { @@ -545,6 +546,30 @@ describe('SubscriptionController', () => { }, ); }); + + it('should update state when lastSubscription changes from undefined to defined', async () => { + await withController( + { + state: { + lastSubscription: undefined, + }, + }, + async ({ controller, mockService }) => { + mockService.getSubscriptions.mockResolvedValue({ + customerId: 'cus_1', + subscriptions: [], + trialedProducts: [], + lastSubscription: MOCK_SUBSCRIPTION, + }); + + await controller.getSubscriptions(); + + expect(controller.state.lastSubscription).toStrictEqual( + MOCK_SUBSCRIPTION, + ); + }, + ); + }); }); describe('getSubscriptionByProduct', () => { diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index 12d56ad3e9d..0c5f040ff71 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -47,7 +47,8 @@ export type SubscriptionControllerState = { trialedProducts: ProductType[]; subscriptions: Subscription[]; pricing?: PricingResponse; - + /** The last subscription that user has subscribed to if any. */ + lastSubscription?: Subscription; /** * The last selected payment method for the user. * This is used to display the last selected payment method in the UI. @@ -199,6 +200,12 @@ const subscriptionControllerMetadata: StateMetadata includeInDebugSnapshot: false, usedInUi: true, }, + lastSubscription: { + includeInStateLogs: true, + persist: true, + includeInDebugSnapshot: false, + usedInUi: true, + }, customerId: { includeInStateLogs: true, persist: true, @@ -342,10 +349,12 @@ export class SubscriptionController extends StaticIntervalPollingController()< const currentSubscriptions = this.state.subscriptions; const currentTrialedProducts = this.state.trialedProducts; const currentCustomerId = this.state.customerId; + const currentLastSubscription = this.state.lastSubscription; const { customerId: newCustomerId, subscriptions: newSubscriptions, trialedProducts: newTrialedProducts, + lastSubscription: newLastSubscription, } = await this.#subscriptionService.getSubscriptions(); // check if the new subscriptions are different from the current subscriptions @@ -358,6 +367,11 @@ export class SubscriptionController extends StaticIntervalPollingController()< currentTrialedProducts, newTrialedProducts, ); + // check if the new last subscription is different from the current last subscription + const isLastSubscriptionEqual = this.#isSubscriptionEqual( + currentLastSubscription, + newLastSubscription, + ); const areCustomerIdsEqual = currentCustomerId === newCustomerId; @@ -365,6 +379,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< // this prevents unnecessary state updates events, easier for the clients to handle if ( !areSubscriptionsEqual || + !isLastSubscriptionEqual || !areTrialedProductsEqual || !areCustomerIdsEqual ) { @@ -372,6 +387,7 @@ export class SubscriptionController extends StaticIntervalPollingController()< state.subscriptions = newSubscriptions; state.customerId = newCustomerId; state.trialedProducts = newTrialedProducts; + state.lastSubscription = newLastSubscription; }); this.#shouldCallRefreshAuthToken = true; } @@ -891,13 +907,25 @@ export class SubscriptionController extends StaticIntervalPollingController()< // Check if all subscriptions are equal return sortedOldSubs.every((oldSub, index) => { const newSub = sortedNewSubs[index]; - return ( - this.#stringifySubscription(oldSub) === - this.#stringifySubscription(newSub) - ); + return this.#isSubscriptionEqual(oldSub, newSub); }); } + #isSubscriptionEqual(oldSub?: Subscription, newSub?: Subscription): boolean { + // not equal if one is undefined and the other is defined + if (!oldSub || !newSub) { + if (!oldSub && !newSub) { + return true; + } + return false; + } + + return ( + this.#stringifySubscription(oldSub) === + this.#stringifySubscription(newSub) + ); + } + #stringifySubscription(subscription: Subscription): string { const subsWithSortedProducts = { ...subscription, diff --git a/packages/subscription-controller/src/SubscriptionService.test.ts b/packages/subscription-controller/src/SubscriptionService.test.ts index 1a0b26a09cc..7af56904b30 100644 --- a/packages/subscription-controller/src/SubscriptionService.test.ts +++ b/packages/subscription-controller/src/SubscriptionService.test.ts @@ -53,6 +53,7 @@ const MOCK_SUBSCRIPTION: Subscription = { last4: '1234', }, }, + isEligibleForSupport: true, }; const MOCK_ACCESS_TOKEN = 'mock-access-token-12345'; diff --git a/packages/subscription-controller/src/types.ts b/packages/subscription-controller/src/types.ts index 518058c6675..f007d2de68a 100644 --- a/packages/subscription-controller/src/types.ts +++ b/packages/subscription-controller/src/types.ts @@ -79,6 +79,12 @@ export type Subscription = { trialEnd?: string; // ISO 8601 /** Crypto payment only: next billing cycle date (e.g after 12 months) */ endDate?: string; // ISO 8601 + /** The date the subscription was canceled. */ + canceledAt?: string; // ISO 8601 + /** The date the subscription was marked as inactive (paused/past_due/canceled). */ + inactiveAt?: string; // ISO 8601 + /** Whether the user is eligible for support features (priority support and filing claims). True for active subscriptions and inactive subscriptions within grace period. */ + isEligibleForSupport: boolean; billingCycles?: number; }; @@ -110,6 +116,8 @@ export type GetSubscriptionsResponse = { customerId?: string; subscriptions: Subscription[]; trialedProducts: ProductType[]; + /** The last subscription that user has subscribed to if any. */ + lastSubscription?: Subscription; }; export type StartSubscriptionRequest = { From 3b6743c02f2311f12baeff85c25f5db1b28e1661 Mon Sep 17 00:00:00 2001 From: Tuna Date: Tue, 11 Nov 2025 16:42:38 +0700 Subject: [PATCH 2/4] 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 5bd5f4153a4..be5e286ae56 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 `lastSubscription` in state returned from `getSubscriptions` method ([#7110](https://github.com/MetaMask/core/pull/7110)) + ## [3.3.0] ### Changed From 0cfbd575e530fa652abe00c7ddc28a66bf03ea8b Mon Sep 17 00:00:00 2001 From: Tuna Date: Tue, 11 Nov 2025 16:47:47 +0700 Subject: [PATCH 3/4] fix: remove subscription from state log --- .../subscription-controller/src/SubscriptionController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subscription-controller/src/SubscriptionController.ts b/packages/subscription-controller/src/SubscriptionController.ts index 0c5f040ff71..00e6c76665e 100644 --- a/packages/subscription-controller/src/SubscriptionController.ts +++ b/packages/subscription-controller/src/SubscriptionController.ts @@ -195,13 +195,13 @@ export function getDefaultSubscriptionControllerState(): SubscriptionControllerS const subscriptionControllerMetadata: StateMetadata = { subscriptions: { - includeInStateLogs: true, + includeInStateLogs: false, persist: true, includeInDebugSnapshot: false, usedInUi: true, }, lastSubscription: { - includeInStateLogs: true, + includeInStateLogs: false, persist: true, includeInDebugSnapshot: false, usedInUi: true, From d9c1f29555eb864eca0cf1b711b9f8a20c86cf74 Mon Sep 17 00:00:00 2001 From: Tuna Date: Tue, 11 Nov 2025 16:51:27 +0700 Subject: [PATCH 4/4] fix: test case --- .../subscription-controller/src/SubscriptionController.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/subscription-controller/src/SubscriptionController.test.ts b/packages/subscription-controller/src/SubscriptionController.test.ts index ea9bfe1f041..d5e7bceb333 100644 --- a/packages/subscription-controller/src/SubscriptionController.test.ts +++ b/packages/subscription-controller/src/SubscriptionController.test.ts @@ -1276,7 +1276,6 @@ describe('SubscriptionController', () => { ), ).toMatchInlineSnapshot(` Object { - "subscriptions": Array [], "trialedProducts": Array [], } `);