Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions packages/subscription-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Added the new controller state, `lastSelectedPaymentMethod`. ([#6946](https://github.com/MetaMask/core/pull/6946))
- We will use this in the UI state persistence between navigation.
- We will use this to query user subscription plan details in subscribe methods internally.

## [3.0.0]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
UpdatePaymentMethodOpts,
Product,
SubscriptionEligibility,
CachedLastSelectedPaymentMethods,
} from './types';
import {
PAYMENT_TYPES,
Expand Down Expand Up @@ -1421,4 +1422,72 @@ describe('SubscriptionController', () => {
});
});
});

describe('cacheLastSelectedPaymentMethod', () => {
it('should cache last selected payment method successfully', async () => {
await withController(async ({ controller }) => {
controller.cacheLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD, {
type: PAYMENT_TYPES.byCard,
plan: RECURRING_INTERVALS.month,
});

expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({
[PRODUCT_TYPES.SHIELD]: {
type: PAYMENT_TYPES.byCard,
plan: RECURRING_INTERVALS.month,
},
});
});
});

it('should update the last selected payment method for the same product', 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.cacheLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD, {
type: PAYMENT_TYPES.byCrypto,
paymentTokenAddress: '0x123',
plan: RECURRING_INTERVALS.month,
});

expect(controller.state.lastSelectedPaymentMethod).toStrictEqual({
[PRODUCT_TYPES.SHIELD]: {
type: PAYMENT_TYPES.byCrypto,
paymentTokenAddress: '0x123',
plan: RECURRING_INTERVALS.month,
},
});
},
);
});

it('should throw error when payment token address is not provided for crypto payment', async () => {
await withController(({ controller }) => {
expect(() =>
controller.cacheLastSelectedPaymentMethod(PRODUCT_TYPES.SHIELD, {
type: PAYMENT_TYPES.byCrypto,
plan: RECURRING_INTERVALS.month,
} as CachedLastSelectedPaymentMethods),
).toThrow(
SubscriptionControllerErrorMessage.PaymentTokenAddressRequiredForCrypto,
);
});
});
});
});
48 changes: 48 additions & 0 deletions packages/subscription-controller/src/SubscriptionController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
TokenPaymentInfo,
UpdatePaymentMethodCardResponse,
UpdatePaymentMethodOpts,
CachedLastSelectedPaymentMethods,
} from './types';
import {
PAYMENT_TYPES,
Expand All @@ -38,6 +39,16 @@ export type SubscriptionControllerState = {
trialedProducts: ProductType[];
subscriptions: Subscription[];
pricing?: PricingResponse;

/**
* The last selected payment method for the user.
* This is used to display the last selected payment method in the UI.
* This state is also meant to be used internally to track the last selected payment method for the user. (e.g. for crypto subscriptions)
*/
lastSelectedPaymentMethod?: Record<
ProductType,
CachedLastSelectedPaymentMethods
>;
};

// Messenger Actions
Expand Down Expand Up @@ -185,6 +196,12 @@ const subscriptionControllerMetadata: StateMetadata<SubscriptionControllerState>
includeInDebugSnapshot: true,
usedInUi: true,
},
lastSelectedPaymentMethod: {
includeInStateLogs: false,
persist: true,
includeInDebugSnapshot: false,
usedInUi: true,
},
};

export class SubscriptionController extends StaticIntervalPollingController()<
Expand Down Expand Up @@ -486,6 +503,37 @@ export class SubscriptionController extends StaticIntervalPollingController()<
throw new Error('Invalid payment type');
}

/**
* 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.type - The type of the payment method.
* @param paymentMethod.paymentTokenAddress - The payment token address.
* @param paymentMethod.plan - The plan of the payment method.
* @param paymentMethod.product - The product of the payment method.
*/
cacheLastSelectedPaymentMethod(
product: ProductType,
paymentMethod: CachedLastSelectedPaymentMethods,
) {
if (
paymentMethod.type === PAYMENT_TYPES.byCrypto &&
!paymentMethod.paymentTokenAddress
) {
throw new Error(
SubscriptionControllerErrorMessage.PaymentTokenAddressRequiredForCrypto,
);
}

this.update((state) => {
state.lastSelectedPaymentMethod = {
...state.lastSelectedPaymentMethod,
[product]: paymentMethod,
};
});
}

/**
* Submit a user event from the UI. (e.g. shield modal viewed)
*
Expand Down
1 change: 1 addition & 0 deletions packages/subscription-controller/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export enum SubscriptionControllerErrorMessage {
UserAlreadySubscribed = `${controllerName} - User is already subscribed`,
UserNotSubscribed = `${controllerName} - User is not subscribed`,
SubscriptionProductsEmpty = `${controllerName} - Subscription products array cannot be empty`,
PaymentTokenAddressRequiredForCrypto = `${controllerName} - Payment token address is required for crypto payment`,
}

export const DEFAULT_POLLING_INTERVAL = 5 * 60 * 1_000; // 5 minutes
1 change: 1 addition & 0 deletions packages/subscription-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export type {
UpdatePaymentMethodCryptoRequest,
UpdatePaymentMethodCardRequest,
UpdatePaymentMethodCardResponse,
CachedLastSelectedPaymentMethods,
} from './types';
export {
CRYPTO_PAYMENT_METHOD_ERRORS,
Expand Down
10 changes: 10 additions & 0 deletions packages/subscription-controller/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,13 @@ export type UpdatePaymentMethodCryptoRequest = {
export type BillingPortalResponse = {
url: string;
};

/**
* The cached result of last selected payment methods for the user.
* These details are being cached to be used internally to track the last selected payment method for the user. (e.g. for crypto subscriptions)
*/
export type CachedLastSelectedPaymentMethods = {
type: PaymentType;
paymentTokenAddress?: Hex;
plan: RecurringInterval;
};
Loading