@metamask-previews/subscription-controller 8.0.1-preview-3866c0ff1 → 8.0.1-preview-e3275932a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/dist/SubscriptionController-method-action-types.cjs +7 -0
  3. package/dist/SubscriptionController-method-action-types.cjs.map +1 -0
  4. package/dist/{SubscriptionController-method-action-types.d.ts → SubscriptionController-method-action-types.d.cts} +2 -2
  5. package/dist/SubscriptionController-method-action-types.d.cts.map +1 -0
  6. package/dist/SubscriptionController-method-action-types.d.mts +250 -0
  7. package/dist/SubscriptionController-method-action-types.d.mts.map +1 -0
  8. package/dist/{SubscriptionController-method-action-types.js → SubscriptionController-method-action-types.mjs} +1 -1
  9. package/dist/SubscriptionController-method-action-types.mjs.map +1 -0
  10. package/dist/SubscriptionController.cjs +744 -0
  11. package/dist/SubscriptionController.cjs.map +1 -0
  12. package/dist/{SubscriptionController.d.ts → SubscriptionController.d.cts} +18 -18
  13. package/dist/SubscriptionController.d.cts.map +1 -0
  14. package/dist/SubscriptionController.d.mts +258 -0
  15. package/dist/SubscriptionController.d.mts.map +1 -0
  16. package/dist/{SubscriptionController.js → SubscriptionController.mjs} +180 -262
  17. package/dist/SubscriptionController.mjs.map +1 -0
  18. package/dist/SubscriptionService-method-action-types.cjs +7 -0
  19. package/dist/SubscriptionService-method-action-types.cjs.map +1 -0
  20. package/dist/{SubscriptionService-method-action-types.d.ts → SubscriptionService-method-action-types.d.cts} +2 -2
  21. package/dist/SubscriptionService-method-action-types.d.cts.map +1 -0
  22. package/dist/SubscriptionService-method-action-types.d.mts +155 -0
  23. package/dist/SubscriptionService-method-action-types.d.mts.map +1 -0
  24. package/dist/{SubscriptionService-method-action-types.js → SubscriptionService-method-action-types.mjs} +1 -1
  25. package/dist/SubscriptionService-method-action-types.mjs.map +1 -0
  26. package/dist/SubscriptionService-structs.cjs +158 -0
  27. package/dist/SubscriptionService-structs.cjs.map +1 -0
  28. package/dist/{SubscriptionService-structs.d.ts → SubscriptionService-structs.d.cts} +134 -134
  29. package/dist/SubscriptionService-structs.d.cts.map +1 -0
  30. package/dist/SubscriptionService-structs.d.mts +721 -0
  31. package/dist/SubscriptionService-structs.d.mts.map +1 -0
  32. package/dist/{SubscriptionService-structs.js → SubscriptionService-structs.mjs} +4 -4
  33. package/dist/SubscriptionService-structs.mjs.map +1 -0
  34. package/dist/SubscriptionService.cjs +477 -0
  35. package/dist/SubscriptionService.cjs.map +1 -0
  36. package/dist/{SubscriptionService.d.ts → SubscriptionService.d.cts} +10 -10
  37. package/dist/SubscriptionService.d.cts.map +1 -0
  38. package/dist/SubscriptionService.d.mts +149 -0
  39. package/dist/SubscriptionService.d.mts.map +1 -0
  40. package/dist/{SubscriptionService.js → SubscriptionService.mjs} +141 -139
  41. package/dist/SubscriptionService.mjs.map +1 -0
  42. package/dist/constants.cjs +73 -0
  43. package/dist/constants.cjs.map +1 -0
  44. package/dist/{constants.d.ts → constants.d.cts} +1 -1
  45. package/dist/constants.d.cts.map +1 -0
  46. package/dist/constants.d.mts +49 -0
  47. package/dist/constants.d.mts.map +1 -0
  48. package/dist/{constants.js → constants.mjs} +3 -3
  49. package/dist/constants.mjs.map +1 -0
  50. package/dist/errors.cjs +72 -0
  51. package/dist/errors.cjs.map +1 -0
  52. package/dist/{errors.d.ts → errors.d.cts} +2 -2
  53. package/dist/errors.d.cts.map +1 -0
  54. package/dist/errors.d.mts +34 -0
  55. package/dist/errors.d.mts.map +1 -0
  56. package/dist/{errors.js → errors.mjs} +1 -5
  57. package/dist/errors.mjs.map +1 -0
  58. package/dist/index.cjs +29 -0
  59. package/dist/index.cjs.map +1 -0
  60. package/dist/{index.d.ts → index.d.cts} +11 -11
  61. package/dist/index.d.cts.map +1 -0
  62. package/dist/index.d.mts +11 -0
  63. package/dist/index.d.mts.map +1 -0
  64. package/dist/{index.js → index.mjs} +6 -6
  65. package/dist/index.mjs.map +1 -0
  66. package/dist/logger.cjs +8 -0
  67. package/dist/logger.cjs.map +1 -0
  68. package/dist/logger.d.cts +5 -0
  69. package/dist/logger.d.cts.map +1 -0
  70. package/dist/logger.d.mts +5 -0
  71. package/dist/logger.d.mts.map +1 -0
  72. package/dist/logger.mjs +5 -0
  73. package/dist/logger.mjs.map +1 -0
  74. package/dist/types.cjs +90 -0
  75. package/dist/types.cjs.map +1 -0
  76. package/dist/{types.d.ts → types.d.cts} +41 -41
  77. package/dist/types.d.cts.map +1 -0
  78. package/dist/types.d.mts +574 -0
  79. package/dist/types.d.mts.map +1 -0
  80. package/dist/{types.js → types.mjs} +1 -1
  81. package/dist/types.mjs.map +1 -0
  82. package/package.json +16 -12
  83. package/dist/SubscriptionController-method-action-types.d.ts.map +0 -1
  84. package/dist/SubscriptionController-method-action-types.js.map +0 -1
  85. package/dist/SubscriptionController.d.ts.map +0 -1
  86. package/dist/SubscriptionController.js.map +0 -1
  87. package/dist/SubscriptionService-method-action-types.d.ts.map +0 -1
  88. package/dist/SubscriptionService-method-action-types.js.map +0 -1
  89. package/dist/SubscriptionService-structs.d.ts.map +0 -1
  90. package/dist/SubscriptionService-structs.js.map +0 -1
  91. package/dist/SubscriptionService.d.ts.map +0 -1
  92. package/dist/SubscriptionService.js.map +0 -1
  93. package/dist/constants.d.ts.map +0 -1
  94. package/dist/constants.js.map +0 -1
  95. package/dist/errors.d.ts.map +0 -1
  96. package/dist/errors.js.map +0 -1
  97. package/dist/index.d.ts.map +0 -1
  98. package/dist/index.js.map +0 -1
  99. package/dist/logger.d.ts +0 -4
  100. package/dist/logger.d.ts.map +0 -1
  101. package/dist/logger.js +0 -5
  102. package/dist/logger.js.map +0 -1
  103. package/dist/types.d.ts.map +0 -1
  104. package/dist/types.js.map +0 -1
@@ -0,0 +1,744 @@
1
+ "use strict";
2
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
5
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
6
+ };
7
+ var _SubscriptionController_instances, _SubscriptionController_getSubscriptionPriceAmount, _SubscriptionController_getSubscriptionBalanceAmount, _SubscriptionController_getProductPriceByProductAndPlan, _SubscriptionController_assertValidSubscriptionStateForCryptoApproval, _SubscriptionController_assertIsUserNotSubscribed, _SubscriptionController_assertIsUserSubscribed, _SubscriptionController_assertIsPaymentMethodCrypto, _SubscriptionController_getIsEligibleForTrialedSponsorship, _SubscriptionController_getIsTrialRequested, _SubscriptionController_findCryptoPaymentMethod, _SubscriptionController_resolveCryptoPaymentMethodDefaults, _SubscriptionController_getChainSupportsSponsorship, _SubscriptionController_areTrialedProductsEqual, _SubscriptionController_areSubscriptionsEqual, _SubscriptionController_isSubscriptionEqual, _SubscriptionController_stringifySubscription;
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.SubscriptionController = exports.getDefaultSubscriptionControllerState = void 0;
10
+ const polling_controller_1 = require("@metamask/polling-controller");
11
+ const transaction_controller_1 = require("@metamask/transaction-controller");
12
+ const bignumber_js_1 = require("bignumber.js");
13
+ const constants_js_1 = require("./constants.cjs");
14
+ const types_js_1 = require("./types.cjs");
15
+ /**
16
+ * Get the default state for the Subscription Controller.
17
+ *
18
+ * @returns The default state for the Subscription Controller.
19
+ */
20
+ function getDefaultSubscriptionControllerState() {
21
+ return {
22
+ subscriptions: [],
23
+ trialedProducts: [],
24
+ };
25
+ }
26
+ exports.getDefaultSubscriptionControllerState = getDefaultSubscriptionControllerState;
27
+ /**
28
+ * Seedless Onboarding Controller State Metadata.
29
+ *
30
+ * This allows us to choose if fields of the state should be persisted or not
31
+ * using the `persist` flag; and if they can be sent to Sentry or not, using
32
+ * the `anonymous` flag.
33
+ */
34
+ const subscriptionControllerMetadata = {
35
+ subscriptions: {
36
+ includeInStateLogs: false,
37
+ persist: true,
38
+ includeInDebugSnapshot: false,
39
+ usedInUi: true,
40
+ },
41
+ lastSubscription: {
42
+ includeInStateLogs: false,
43
+ persist: true,
44
+ includeInDebugSnapshot: false,
45
+ usedInUi: true,
46
+ },
47
+ customerId: {
48
+ includeInStateLogs: true,
49
+ persist: true,
50
+ includeInDebugSnapshot: false,
51
+ usedInUi: true,
52
+ },
53
+ rewardAccountId: {
54
+ includeInStateLogs: true,
55
+ persist: true,
56
+ includeInDebugSnapshot: false,
57
+ usedInUi: true,
58
+ },
59
+ trialedProducts: {
60
+ includeInStateLogs: true,
61
+ persist: true,
62
+ includeInDebugSnapshot: true,
63
+ usedInUi: true,
64
+ },
65
+ pricing: {
66
+ includeInStateLogs: true,
67
+ persist: true,
68
+ includeInDebugSnapshot: true,
69
+ usedInUi: true,
70
+ },
71
+ lastSelectedPaymentMethod: {
72
+ includeInStateLogs: false,
73
+ persist: true,
74
+ includeInDebugSnapshot: false,
75
+ usedInUi: true,
76
+ },
77
+ };
78
+ const MESSENGER_EXPOSED_METHODS = [
79
+ 'getPricing',
80
+ 'getSubscriptions',
81
+ 'getSubscriptionByProduct',
82
+ 'getSubscriptionsEligibilities',
83
+ 'cancelSubscription',
84
+ 'unCancelSubscription',
85
+ 'startSubscriptionWithCard',
86
+ 'startSubscriptionWithCrypto',
87
+ 'stopAllPolling',
88
+ 'submitSubscriptionCryptoApproval',
89
+ 'getCryptoApproveTransactionParams',
90
+ 'updatePaymentMethod',
91
+ 'getBillingPortalUrl',
92
+ 'cacheLastSelectedPaymentMethod',
93
+ 'clearLastSelectedPaymentMethod',
94
+ 'submitSponsorshipIntents',
95
+ 'submitUserEvent',
96
+ 'assignUserToCohort',
97
+ 'linkRewards',
98
+ 'getTokenApproveAmount',
99
+ 'getTokenMinimumBalanceAmount',
100
+ 'clearState',
101
+ 'triggerAccessTokenRefresh',
102
+ ];
103
+ class SubscriptionController extends (0, polling_controller_1.StaticIntervalPollingController)() {
104
+ /**
105
+ * Creates a new SubscriptionController instance.
106
+ *
107
+ * @param options - The options for the SubscriptionController.
108
+ * @param options.messenger - A restricted messenger.
109
+ * @param options.state - Initial state to set on this controller.
110
+ * @param options.pollingInterval - The polling interval to use for the subscription controller.
111
+ */
112
+ constructor(options) {
113
+ const { messenger, state, pollingInterval = constants_js_1.DEFAULT_POLLING_INTERVAL, } = options;
114
+ super({
115
+ name: constants_js_1.controllerName,
116
+ metadata: subscriptionControllerMetadata,
117
+ state: {
118
+ ...getDefaultSubscriptionControllerState(),
119
+ ...state,
120
+ },
121
+ messenger,
122
+ });
123
+ _SubscriptionController_instances.add(this);
124
+ this.setIntervalLength(pollingInterval);
125
+ this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
126
+ }
127
+ /**
128
+ * Gets the pricing information from the subscription service.
129
+ *
130
+ * @returns The pricing information.
131
+ */
132
+ async getPricing() {
133
+ const pricing = await this.messenger.call('SubscriptionService:getPricing');
134
+ this.update((state) => {
135
+ state.pricing = pricing;
136
+ });
137
+ return pricing;
138
+ }
139
+ async getSubscriptions() {
140
+ const currentSubscriptions = this.state.subscriptions;
141
+ const currentTrialedProducts = this.state.trialedProducts;
142
+ const currentCustomerId = this.state.customerId;
143
+ const currentLastSubscription = this.state.lastSubscription;
144
+ const currentRewardAccountId = this.state.rewardAccountId;
145
+ const { customerId: newCustomerId, subscriptions: newSubscriptions, trialedProducts: newTrialedProducts, lastSubscription: newLastSubscription, rewardAccountId: newRewardAccountId, } = await this.messenger.call('SubscriptionService:getSubscriptions');
146
+ // check if the new subscriptions are different from the current subscriptions
147
+ const areSubscriptionsEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_areSubscriptionsEqual).call(this, currentSubscriptions, newSubscriptions);
148
+ // check if the new trialed products are different from the current trialed products
149
+ const areTrialedProductsEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_areTrialedProductsEqual).call(this, currentTrialedProducts, newTrialedProducts);
150
+ // check if the new last subscription is different from the current last subscription
151
+ const isLastSubscriptionEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_isSubscriptionEqual).call(this, currentLastSubscription, newLastSubscription);
152
+ const areCustomerIdsEqual = currentCustomerId === newCustomerId;
153
+ const areRewardAccountIdsEqual = currentRewardAccountId === newRewardAccountId;
154
+ // only update the state if the subscriptions or trialed products are different
155
+ // this prevents unnecessary state updates events, easier for the clients to handle
156
+ if (!areSubscriptionsEqual ||
157
+ !isLastSubscriptionEqual ||
158
+ !areTrialedProductsEqual ||
159
+ !areCustomerIdsEqual ||
160
+ !areRewardAccountIdsEqual) {
161
+ this.update((state) => {
162
+ state.subscriptions = newSubscriptions;
163
+ state.customerId = newCustomerId;
164
+ state.trialedProducts = newTrialedProducts;
165
+ state.lastSubscription = newLastSubscription;
166
+ state.rewardAccountId = newRewardAccountId;
167
+ });
168
+ // trigger access token refresh to ensure the user has the latest access token if subscription state change
169
+ this.triggerAccessTokenRefresh();
170
+ }
171
+ return newSubscriptions;
172
+ }
173
+ /**
174
+ * Get the subscription by product.
175
+ *
176
+ * @param productType - The product type.
177
+ * @returns The subscription.
178
+ */
179
+ getSubscriptionByProduct(productType) {
180
+ return this.state.subscriptions.find((subscription) => subscription.products.some((product) => product.name === productType));
181
+ }
182
+ /**
183
+ * Get the subscriptions eligibilities.
184
+ *
185
+ * @param request - Optional request object containing user balance to check cohort eligibility.
186
+ * @returns The subscriptions eligibilities.
187
+ */
188
+ async getSubscriptionsEligibilities(request) {
189
+ return await this.messenger.call('SubscriptionService:getSubscriptionsEligibilities', request);
190
+ }
191
+ async cancelSubscription(request) {
192
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
193
+ const cancelledSubscription = await this.messenger.call('SubscriptionService:cancelSubscription', request);
194
+ this.update((state) => {
195
+ state.subscriptions = state.subscriptions.map((subscription) => subscription.id === request.subscriptionId
196
+ ? { ...subscription, ...cancelledSubscription }
197
+ : subscription);
198
+ });
199
+ this.triggerAccessTokenRefresh();
200
+ }
201
+ async unCancelSubscription(request) {
202
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
203
+ const uncancelledSubscription = await this.messenger.call('SubscriptionService:unCancelSubscription', {
204
+ subscriptionId: request.subscriptionId,
205
+ });
206
+ this.update((state) => {
207
+ state.subscriptions = state.subscriptions.map((subscription) => subscription.id === request.subscriptionId
208
+ ? { ...subscription, ...uncancelledSubscription }
209
+ : subscription);
210
+ });
211
+ this.triggerAccessTokenRefresh();
212
+ }
213
+ /**
214
+ * Starts a card-paid subscription checkout session for the requested products
215
+ * (e.g. Shield or Money Account Plus).
216
+ *
217
+ * `isTrialRequested` on the request is ignored and overwritten from pricing
218
+ * (`trialPeriodDays > 0`) and `trialedProducts`.
219
+ *
220
+ * @param request - The start subscription request.
221
+ * @returns The checkout session response.
222
+ */
223
+ async startSubscriptionWithCard(request) {
224
+ // get the latest subscriptions state before computing trial eligibility
225
+ await this.getSubscriptions();
226
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
227
+ const response = await this.messenger.call('SubscriptionService:startSubscriptionWithCard', {
228
+ ...request,
229
+ isTrialRequested: __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, request.products, request.recurringInterval),
230
+ });
231
+ // note: no need to trigger access token refresh after startSubscriptionWithCard request because this only return stripe checkout session url, subscription not created yet
232
+ return response;
233
+ }
234
+ /**
235
+ * Starts a crypto-paid subscription for the requested products
236
+ * (e.g. Shield or Money Account Plus). Unlike card checkout, this
237
+ * creates the subscription immediately, so local state is refreshed
238
+ * afterwards.
239
+ *
240
+ * `isTrialRequested` on the request is ignored and overwritten from pricing
241
+ * (`trialPeriodDays > 0`) and `trialedProducts`.
242
+ *
243
+ * @param request - The start crypto subscription request.
244
+ * @returns The start crypto subscription response.
245
+ * @throws If `products` is empty.
246
+ */
247
+ async startSubscriptionWithCrypto(request) {
248
+ if (request.products.length === 0) {
249
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.SubscriptionProductsEmpty);
250
+ }
251
+ // get the latest subscriptions state before computing trial eligibility
252
+ await this.getSubscriptions();
253
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
254
+ const response = await this.messenger.call('SubscriptionService:startSubscriptionWithCrypto', {
255
+ ...request,
256
+ isTrialRequested: __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, request.products, request.recurringInterval),
257
+ });
258
+ // Crypto start creates the subscription immediately (unlike card checkout).
259
+ await this.getSubscriptions();
260
+ return response;
261
+ }
262
+ /**
263
+ * Submits a Shield ERC-20 crypto approval transaction to start or update a
264
+ * crypto subscription.
265
+ *
266
+ * This handler is Shield / `TransactionType.shieldSubscriptionApprove` only.
267
+ * Delegation-based products (e.g. Money Account) must call
268
+ * `startSubscriptionWithCrypto` instead.
269
+ *
270
+ * @param request - The crypto approval request.
271
+ * @param request.productType - The subscription product. Typed as
272
+ * `typeof PRODUCT_TYPES.SHIELD` only at the moment (future might support more
273
+ * product).
274
+ * @param request.txMeta - The transaction metadata. Must have type
275
+ * `TransactionType.shieldSubscriptionApprove`.
276
+ * @param request.isSponsored - Whether the transaction is sponsored.
277
+ * @param request.rewardAccountId - The account ID of the reward subscription
278
+ * to link.
279
+ * @throws If `productType` is not Shield or `txMeta.type` is not
280
+ * `shieldSubscriptionApprove`.
281
+ * @returns void
282
+ */
283
+ async submitSubscriptionCryptoApproval(request) {
284
+ const { productType, txMeta, isSponsored, rewardAccountId } = request;
285
+ if (
286
+ // Widen for the runtime guard: JS / unsound callers may still pass a
287
+ // non-Shield product.
288
+ productType !== types_js_1.PRODUCT_TYPES.SHIELD ||
289
+ txMeta.type !== transaction_controller_1.TransactionType.shieldSubscriptionApprove) {
290
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.CryptoApprovalRequiresShieldApprove);
291
+ }
292
+ const { chainId, rawTx } = txMeta;
293
+ if (!chainId || !rawTx) {
294
+ throw new Error('Chain ID or raw transaction not found');
295
+ }
296
+ const { pricing, lastSelectedPaymentMethod } = this.state;
297
+ if (!pricing) {
298
+ throw new Error('Subscription pricing not found');
299
+ }
300
+ if (!lastSelectedPaymentMethod) {
301
+ throw new Error('Last selected payment method not found');
302
+ }
303
+ const lastSelectedPaymentMethodForProduct = lastSelectedPaymentMethod[productType];
304
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsPaymentMethodCrypto).call(this, lastSelectedPaymentMethodForProduct);
305
+ const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this, productType, lastSelectedPaymentMethodForProduct.plan);
306
+ // get the latest subscriptions state before computing trial eligibility
307
+ await this.getSubscriptions();
308
+ const isTrialRequested = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, [productType], lastSelectedPaymentMethodForProduct.plan);
309
+ const currentSubscription = this.getSubscriptionByProduct(productType);
310
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertValidSubscriptionStateForCryptoApproval).call(this, {
311
+ productType,
312
+ });
313
+ // if subscription exists, this transaction is for changing payment method
314
+ const isChangePaymentMethod = Boolean(currentSubscription);
315
+ if (isChangePaymentMethod) {
316
+ await this.updatePaymentMethod({
317
+ paymentType: types_js_1.PAYMENT_TYPES.byCrypto,
318
+ subscriptionId: currentSubscription.id,
319
+ chainId,
320
+ payerAddress: txMeta.txParams.from,
321
+ tokenSymbol: lastSelectedPaymentMethodForProduct.paymentTokenSymbol,
322
+ rawTransaction: rawTx,
323
+ recurringInterval: productPrice.interval,
324
+ billingCycles: productPrice.minBillingCycles,
325
+ });
326
+ }
327
+ else {
328
+ const params = {
329
+ products: [productType],
330
+ isTrialRequested,
331
+ recurringInterval: productPrice.interval,
332
+ billingCycles: productPrice.minBillingCycles,
333
+ chainId,
334
+ payerAddress: txMeta.txParams.from,
335
+ tokenSymbol: lastSelectedPaymentMethodForProduct.paymentTokenSymbol,
336
+ rawTransaction: rawTx,
337
+ cryptoAuthMethod: types_js_1.CRYPTO_AUTH_METHODS.ERC20_APPROVAL,
338
+ isSponsored,
339
+ useTestClock: lastSelectedPaymentMethodForProduct.useTestClock,
340
+ rewardAccountId,
341
+ };
342
+ await this.startSubscriptionWithCrypto(params);
343
+ }
344
+ // update the subscriptions state after subscription created in server
345
+ await this.getSubscriptions();
346
+ }
347
+ /**
348
+ * Get transaction params to create crypto approve transaction for subscription payment
349
+ *
350
+ * @param request - The request object
351
+ * @param request.chainId - The chain ID
352
+ * @param request.tokenAddress - The address of the token
353
+ * @param request.productType - The product type
354
+ * @param request.interval - The interval
355
+ * @returns The crypto approve transaction params
356
+ */
357
+ getCryptoApproveTransactionParams(request) {
358
+ const { pricing } = this.state;
359
+ if (!pricing) {
360
+ throw new Error('Subscription pricing not found');
361
+ }
362
+ const product = pricing.products.find((productInfo) => productInfo.name === request.productType);
363
+ if (!product) {
364
+ throw new Error('Product price not found');
365
+ }
366
+ const price = product.prices.find((productPrice) => productPrice.interval === request.interval);
367
+ if (!price) {
368
+ throw new Error('Price not found');
369
+ }
370
+ const chainsPaymentInfo = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_findCryptoPaymentMethod).call(this, request.productType, types_js_1.CRYPTO_AUTH_METHODS.ERC20_APPROVAL);
371
+ if (!chainsPaymentInfo) {
372
+ throw new Error('Chains payment info not found');
373
+ }
374
+ const chainPaymentInfo = chainsPaymentInfo.chains?.find((chain) => chain.chainId === request.chainId);
375
+ if (!chainPaymentInfo) {
376
+ throw new Error('Invalid chain id');
377
+ }
378
+ const tokenPaymentInfo = chainPaymentInfo.tokens.find((token) => token.address.toLowerCase() ===
379
+ request.paymentTokenAddress.toLowerCase());
380
+ if (!tokenPaymentInfo) {
381
+ throw new Error('Invalid token address');
382
+ }
383
+ const tokenApproveAmount = this.getTokenApproveAmount(price, tokenPaymentInfo);
384
+ return {
385
+ approveAmount: tokenApproveAmount,
386
+ paymentAddress: chainPaymentInfo.paymentAddress,
387
+ paymentTokenAddress: request.paymentTokenAddress,
388
+ chainId: request.chainId,
389
+ };
390
+ }
391
+ async updatePaymentMethod(opts) {
392
+ if (opts.paymentType === types_js_1.PAYMENT_TYPES.byCard) {
393
+ const { paymentType, ...cardRequest } = opts;
394
+ return await this.messenger.call('SubscriptionService:updatePaymentMethodCard', cardRequest);
395
+ }
396
+ else if (opts.paymentType === types_js_1.PAYMENT_TYPES.byCrypto) {
397
+ const { paymentType, ...cryptoRequest } = opts;
398
+ await this.messenger.call('SubscriptionService:updatePaymentMethodCrypto', cryptoRequest);
399
+ return await this.getSubscriptions();
400
+ }
401
+ throw new Error('Invalid payment type');
402
+ }
403
+ /**
404
+ * Gets the billing portal URL.
405
+ *
406
+ * @returns The billing portal URL
407
+ */
408
+ async getBillingPortalUrl() {
409
+ return await this.messenger.call('SubscriptionService:getBillingPortalUrl');
410
+ }
411
+ /**
412
+ * Cache the last selected payment method for a specific product.
413
+ *
414
+ * @param request - The request object.
415
+ * @param request.product - The product to cache the payment method for.
416
+ * @param request.paymentMethod - The payment method to cache.
417
+ * @param request.paymentMethod.type - The type of the payment method.
418
+ * @param request.paymentMethod.paymentTokenAddress - The payment token address.
419
+ * @param request.paymentMethod.plan - The plan of the payment method.
420
+ */
421
+ cacheLastSelectedPaymentMethod(request) {
422
+ const { product, paymentMethod } = request;
423
+ if (paymentMethod.type === types_js_1.PAYMENT_TYPES.byCrypto &&
424
+ (!paymentMethod.paymentTokenAddress || !paymentMethod.paymentTokenSymbol)) {
425
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.PaymentTokenAddressAndSymbolRequiredForCrypto);
426
+ }
427
+ this.update((state) => {
428
+ state.lastSelectedPaymentMethod = {
429
+ ...state.lastSelectedPaymentMethod,
430
+ [product]: paymentMethod,
431
+ };
432
+ });
433
+ }
434
+ /**
435
+ * Clear the last selected payment method for a specific product.
436
+ *
437
+ * @param product - The product to clear the payment method for.
438
+ */
439
+ clearLastSelectedPaymentMethod(product) {
440
+ this.update((state) => {
441
+ if (state.lastSelectedPaymentMethod) {
442
+ const { [product]: _, ...rest } = state.lastSelectedPaymentMethod;
443
+ state.lastSelectedPaymentMethod =
444
+ rest;
445
+ }
446
+ });
447
+ }
448
+ /**
449
+ * Submit sponsorship intents to the Subscription Service backend.
450
+ *
451
+ * This is intended to be used together with the crypto subscription flow.
452
+ * When the user has enabled the smart transaction feature, we will sponsor the gas fees for the subscription approval transaction.
453
+ *
454
+ * @param request - Request object containing the address and products.
455
+ * @example {
456
+ * address: '0x1234567890123456789012345678901234567890',
457
+ * products: [ProductType.Shield],
458
+ * recurringInterval: RecurringInterval.Month,
459
+ * billingCycles: 1,
460
+ * }
461
+ * @returns resolves to true if the sponsorship is supported and intents were submitted successfully, false if the chain does not support sponsorship or the user has already trialed
462
+ * @throws If the crypto payment method or chain is missing from pricing
463
+ */
464
+ async submitSponsorshipIntents(request) {
465
+ if (request.products.length === 0) {
466
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.SubscriptionProductsEmpty);
467
+ }
468
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
469
+ const selectedPaymentMethod = this.state.lastSelectedPaymentMethod?.[request.products[0]];
470
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsPaymentMethodCrypto).call(this, selectedPaymentMethod);
471
+ const cryptoAuthMethod = selectedPaymentMethod.cryptoAuthMethod ??
472
+ types_js_1.CRYPTO_AUTH_METHODS.ERC20_APPROVAL;
473
+ const isEligibleForTrialedSponsorship = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsEligibleForTrialedSponsorship).call(this, request.chainId, request.products, cryptoAuthMethod);
474
+ if (!isEligibleForTrialedSponsorship) {
475
+ return false;
476
+ }
477
+ const { paymentTokenSymbol, plan } = selectedPaymentMethod;
478
+ const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this,
479
+ // we only support one product at a time for now
480
+ request.products[0], plan);
481
+ const billingCycles = productPrice.minBillingCycles;
482
+ await this.messenger.call('SubscriptionService:submitSponsorshipIntents', {
483
+ ...request,
484
+ paymentTokenSymbol,
485
+ billingCycles,
486
+ recurringInterval: plan,
487
+ });
488
+ return true;
489
+ }
490
+ /**
491
+ * Submit a user event from the UI. (e.g. shield modal viewed)
492
+ *
493
+ * @param request - Request object containing the event to submit.
494
+ * @example { event: SubscriptionUserEvent.ShieldEntryModalViewed, cohort: 'post_tx' }
495
+ */
496
+ async submitUserEvent(request) {
497
+ await this.messenger.call('SubscriptionService:submitUserEvent', request);
498
+ }
499
+ /**
500
+ * Assign user to a cohort.
501
+ *
502
+ * @param request - Request object containing the cohort to assign the user to.
503
+ * @example { cohort: 'post_tx' }
504
+ */
505
+ async assignUserToCohort(request) {
506
+ await this.messenger.call('SubscriptionService:assignUserToCohort', request);
507
+ }
508
+ /**
509
+ * Link rewards to a subscription.
510
+ *
511
+ * @param request - Request object containing the reward subscription ID.
512
+ * @param request.subscriptionId - The ID of the subscription to link rewards to.
513
+ * @param request.rewardAccountId - The account ID of the reward subscription to link to the subscription.
514
+ * @example { subscriptionId: '1234567890', rewardAccountId: 'eip155:1:0x1234567890123456789012345678901234567890' }
515
+ * @returns Resolves when the rewards are linked successfully.
516
+ */
517
+ async linkRewards(request) {
518
+ // assert that the user is subscribed to the subscription
519
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
520
+ // link rewards to the subscription
521
+ const response = await this.messenger.call('SubscriptionService:linkRewards', {
522
+ rewardAccountId: request.rewardAccountId,
523
+ });
524
+ if (!response.success) {
525
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.LinkRewardsFailed);
526
+ }
527
+ }
528
+ async _executePoll() {
529
+ await this.getSubscriptions();
530
+ }
531
+ /**
532
+ * Calculate token approve amount from price info
533
+ *
534
+ * @param price - The price info
535
+ * @param tokenPaymentInfo - The token price info
536
+ * @returns The token approve amount
537
+ */
538
+ getTokenApproveAmount(price, tokenPaymentInfo) {
539
+ const conversionRate = tokenPaymentInfo.conversionRate?.[price.currency];
540
+ if (!conversionRate) {
541
+ throw new Error('Conversion rate not found');
542
+ }
543
+ // price of the product
544
+ const priceAmount = new bignumber_js_1.BigNumber(__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getSubscriptionPriceAmount).call(this, price));
545
+ const tokenDecimal = new bignumber_js_1.BigNumber(10).pow(tokenPaymentInfo.decimals);
546
+ const tokenAmount = priceAmount
547
+ .multipliedBy(tokenDecimal)
548
+ .div(conversionRate);
549
+ return tokenAmount.toFixed(0);
550
+ }
551
+ /**
552
+ * Calculate token minimum balance amount from price info
553
+ *
554
+ * @param price - The price info
555
+ * @param tokenPaymentInfo - The token price info
556
+ * @returns The token balance amount
557
+ */
558
+ getTokenMinimumBalanceAmount(price, tokenPaymentInfo) {
559
+ const conversionRate = tokenPaymentInfo.conversionRate?.[price.currency];
560
+ if (!conversionRate) {
561
+ throw new Error('Conversion rate not found');
562
+ }
563
+ const balanceAmount = new bignumber_js_1.BigNumber(__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getSubscriptionBalanceAmount).call(this, price));
564
+ const tokenDecimal = new bignumber_js_1.BigNumber(10).pow(tokenPaymentInfo.decimals);
565
+ const tokenAmount = balanceAmount
566
+ .multipliedBy(tokenDecimal)
567
+ .div(conversionRate);
568
+ return tokenAmount.toFixed(0);
569
+ }
570
+ /**
571
+ * Clears the subscription state and resets to default values.
572
+ */
573
+ clearState() {
574
+ const defaultState = getDefaultSubscriptionControllerState();
575
+ this.update(() => {
576
+ return defaultState;
577
+ });
578
+ }
579
+ /**
580
+ * Triggers an access token refresh.
581
+ */
582
+ triggerAccessTokenRefresh() {
583
+ // We perform a sign out to clear the access token from the authentication
584
+ // controller. Next time the access token is requested, a new access token
585
+ // will be fetched.
586
+ this.messenger.call('AuthenticationController:performSignOut');
587
+ }
588
+ }
589
+ exports.SubscriptionController = SubscriptionController;
590
+ _SubscriptionController_instances = new WeakSet(), _SubscriptionController_getSubscriptionPriceAmount = function _SubscriptionController_getSubscriptionPriceAmount(price) {
591
+ // no need to use BigInt since max unitDecimals are always 2 for price
592
+ const amount = new bignumber_js_1.BigNumber(price.unitAmount)
593
+ .div(10 ** price.unitDecimals)
594
+ .multipliedBy(price.minBillingCycles)
595
+ .toString();
596
+ return amount;
597
+ }, _SubscriptionController_getSubscriptionBalanceAmount = function _SubscriptionController_getSubscriptionBalanceAmount(price) {
598
+ // no need to use BigInt since max unitDecimals are always 2 for price
599
+ const amount = new bignumber_js_1.BigNumber(price.unitAmount)
600
+ .div(10 ** price.unitDecimals)
601
+ .multipliedBy(price.minBillingCyclesForBalance)
602
+ .toString();
603
+ return amount;
604
+ }, _SubscriptionController_getProductPriceByProductAndPlan = function _SubscriptionController_getProductPriceByProductAndPlan(productType, plan) {
605
+ const { pricing } = this.state;
606
+ const productPricing = pricing?.products.find((product) => product.name === productType);
607
+ const productPrice = productPricing?.prices.find((price) => price.interval === plan);
608
+ if (!productPrice) {
609
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.ProductPriceNotFound);
610
+ }
611
+ return productPrice;
612
+ }, _SubscriptionController_assertValidSubscriptionStateForCryptoApproval = function _SubscriptionController_assertValidSubscriptionStateForCryptoApproval({ productType, }) {
613
+ const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => product.name === productType));
614
+ const isValid = !subscription ||
615
+ [
616
+ types_js_1.SUBSCRIPTION_STATUSES.pastDue,
617
+ types_js_1.SUBSCRIPTION_STATUSES.unpaid,
618
+ types_js_1.SUBSCRIPTION_STATUSES.paused,
619
+ types_js_1.SUBSCRIPTION_STATUSES.provisional,
620
+ types_js_1.SUBSCRIPTION_STATUSES.active,
621
+ types_js_1.SUBSCRIPTION_STATUSES.trialing,
622
+ ].includes(subscription.status);
623
+ if (!isValid) {
624
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.SubscriptionNotValidForCryptoApproval);
625
+ }
626
+ }, _SubscriptionController_assertIsUserNotSubscribed = function _SubscriptionController_assertIsUserNotSubscribed({ products }) {
627
+ const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => products.includes(product.name)));
628
+ if (subscription &&
629
+ constants_js_1.ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) {
630
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.UserAlreadySubscribed);
631
+ }
632
+ }, _SubscriptionController_assertIsUserSubscribed = function _SubscriptionController_assertIsUserSubscribed(request) {
633
+ if (!this.state.subscriptions.find((subscription) => subscription.id === request.subscriptionId)) {
634
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.UserNotSubscribed);
635
+ }
636
+ }, _SubscriptionController_assertIsPaymentMethodCrypto = function _SubscriptionController_assertIsPaymentMethodCrypto(value) {
637
+ if (value?.type !== types_js_1.PAYMENT_TYPES.byCrypto ||
638
+ !value.paymentTokenAddress ||
639
+ !value.paymentTokenSymbol) {
640
+ throw new Error(constants_js_1.SubscriptionControllerErrorMessage.PaymentMethodNotCrypto);
641
+ }
642
+ }, _SubscriptionController_getIsEligibleForTrialedSponsorship = function _SubscriptionController_getIsEligibleForTrialedSponsorship(chainId, products, cryptoAuthMethod) {
643
+ const isSponsorshipSupported = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getChainSupportsSponsorship).call(this, chainId, products[0], cryptoAuthMethod);
644
+ // verify if the user has trialed the provided products before
645
+ const hasTrialedBefore = this.state.trialedProducts.some((product) => products.includes(product));
646
+ return isSponsorshipSupported && !hasTrialedBefore;
647
+ }, _SubscriptionController_getIsTrialRequested = function _SubscriptionController_getIsTrialRequested(products, plan) {
648
+ return products.every((productType) => {
649
+ if (this.state.trialedProducts.includes(productType)) {
650
+ return false;
651
+ }
652
+ const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this, productType, plan);
653
+ return productPrice.trialPeriodDays > 0;
654
+ });
655
+ }, _SubscriptionController_findCryptoPaymentMethod = function _SubscriptionController_findCryptoPaymentMethod(productType, cryptoAuthMethod) {
656
+ const matches = [];
657
+ for (const paymentMethod of this.state.pricing?.paymentMethods ?? []) {
658
+ if (paymentMethod.type !== types_js_1.PAYMENT_TYPES.byCrypto) {
659
+ continue;
660
+ }
661
+ const resolved = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_resolveCryptoPaymentMethodDefaults).call(this, paymentMethod);
662
+ if (!resolved) {
663
+ continue;
664
+ }
665
+ if (resolved.cryptoAuthMethod === cryptoAuthMethod &&
666
+ resolved.products.includes(productType)) {
667
+ matches.push({
668
+ method: paymentMethod,
669
+ explicit: resolved.explicit,
670
+ });
671
+ }
672
+ }
673
+ if (matches.length === 0) {
674
+ return undefined;
675
+ }
676
+ const explicitMatches = matches.filter((match) => match.explicit);
677
+ const candidates = explicitMatches.length > 0 ? explicitMatches : matches;
678
+ if (candidates.length > 1) {
679
+ throw new Error('Multiple matching crypto payment methods found');
680
+ }
681
+ return candidates[0].method;
682
+ }, _SubscriptionController_resolveCryptoPaymentMethodDefaults = function _SubscriptionController_resolveCryptoPaymentMethodDefaults(paymentMethod) {
683
+ const productsPresent = paymentMethod.products !== undefined;
684
+ const authPresent = paymentMethod.cryptoAuthMethod !== undefined;
685
+ if (!productsPresent && !authPresent) {
686
+ return {
687
+ products: [types_js_1.PRODUCT_TYPES.SHIELD],
688
+ cryptoAuthMethod: types_js_1.CRYPTO_AUTH_METHODS.ERC20_APPROVAL,
689
+ explicit: false,
690
+ };
691
+ }
692
+ if (!paymentMethod.products?.length || !paymentMethod.cryptoAuthMethod) {
693
+ return undefined;
694
+ }
695
+ return {
696
+ products: paymentMethod.products,
697
+ cryptoAuthMethod: paymentMethod.cryptoAuthMethod,
698
+ explicit: true,
699
+ };
700
+ }, _SubscriptionController_getChainSupportsSponsorship = function _SubscriptionController_getChainSupportsSponsorship(chainId, productType, cryptoAuthMethod) {
701
+ const cryptoPaymentInfo = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_findCryptoPaymentMethod).call(this, productType, cryptoAuthMethod);
702
+ if (!cryptoPaymentInfo) {
703
+ throw new Error('Chains payment info not found');
704
+ }
705
+ const chainPaymentInfo = cryptoPaymentInfo.chains?.find((chain) => chain.chainId === chainId);
706
+ if (!chainPaymentInfo) {
707
+ throw new Error('Invalid chain id');
708
+ }
709
+ return Boolean(chainPaymentInfo.isSponsorshipSupported);
710
+ }, _SubscriptionController_areTrialedProductsEqual = function _SubscriptionController_areTrialedProductsEqual(oldTrialedProducts, newTrialedProducts) {
711
+ return (oldTrialedProducts.length === newTrialedProducts?.length &&
712
+ oldTrialedProducts.every((product) => newTrialedProducts?.includes(product)));
713
+ }, _SubscriptionController_areSubscriptionsEqual = function _SubscriptionController_areSubscriptionsEqual(oldSubs, newSubs) {
714
+ // Check if arrays have different lengths
715
+ if (oldSubs.length !== newSubs.length) {
716
+ return false;
717
+ }
718
+ // Sort both arrays by id to ensure consistent comparison
719
+ const sortedOldSubs = [...oldSubs].sort((a, b) => a.id.localeCompare(b.id));
720
+ const sortedNewSubs = [...newSubs].sort((a, b) => a.id.localeCompare(b.id));
721
+ // Check if all subscriptions are equal
722
+ return sortedOldSubs.every((oldSub, index) => {
723
+ const newSub = sortedNewSubs[index];
724
+ return __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_isSubscriptionEqual).call(this, oldSub, newSub);
725
+ });
726
+ }, _SubscriptionController_isSubscriptionEqual = function _SubscriptionController_isSubscriptionEqual(oldSub, newSub) {
727
+ // not equal if one is undefined and the other is defined
728
+ if (!oldSub || !newSub) {
729
+ if (!oldSub && !newSub) {
730
+ return true;
731
+ }
732
+ return false;
733
+ }
734
+ return (__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_stringifySubscription).call(this, oldSub) ===
735
+ __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_stringifySubscription).call(this, newSub));
736
+ }, _SubscriptionController_stringifySubscription = function _SubscriptionController_stringifySubscription(subscription) {
737
+ const subsWithSortedProducts = {
738
+ ...subscription,
739
+ // order the products by name
740
+ products: [...subscription.products].sort((a, b) => a.name.localeCompare(b.name)),
741
+ };
742
+ return JSON.stringify(subsWithSortedProducts);
743
+ };
744
+ //# sourceMappingURL=SubscriptionController.cjs.map