@metamask/subscription-controller 8.0.1 → 9.0.0

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 (110) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/dist/{SubscriptionController-method-action-types.d.mts → SubscriptionController-method-action-types.d.ts} +14 -3
  3. package/dist/SubscriptionController-method-action-types.d.ts.map +1 -0
  4. package/dist/{SubscriptionController-method-action-types.mjs → SubscriptionController-method-action-types.js} +1 -1
  5. package/dist/SubscriptionController-method-action-types.js.map +1 -0
  6. package/dist/{SubscriptionController.d.mts → SubscriptionController.d.ts} +29 -19
  7. package/dist/SubscriptionController.d.ts.map +1 -0
  8. package/dist/{SubscriptionController.mjs → SubscriptionController.js} +391 -190
  9. package/dist/SubscriptionController.js.map +1 -0
  10. package/dist/{SubscriptionService-method-action-types.d.mts → SubscriptionService-method-action-types.d.ts} +12 -3
  11. package/dist/SubscriptionService-method-action-types.d.ts.map +1 -0
  12. package/dist/{SubscriptionService-method-action-types.mjs → SubscriptionService-method-action-types.js} +1 -1
  13. package/dist/SubscriptionService-method-action-types.js.map +1 -0
  14. package/dist/{SubscriptionService-structs.d.mts → SubscriptionService-structs.d.ts} +302 -134
  15. package/dist/SubscriptionService-structs.d.ts.map +1 -0
  16. package/dist/{SubscriptionService-structs.mjs → SubscriptionService-structs.js} +55 -4
  17. package/dist/SubscriptionService-structs.js.map +1 -0
  18. package/dist/{SubscriptionService.d.cts → SubscriptionService.d.ts} +16 -10
  19. package/dist/SubscriptionService.d.ts.map +1 -0
  20. package/dist/{SubscriptionService.mjs → SubscriptionService.js} +159 -141
  21. package/dist/SubscriptionService.js.map +1 -0
  22. package/dist/{constants.d.cts → constants.d.ts} +2 -1
  23. package/dist/constants.d.ts.map +1 -0
  24. package/dist/{constants.mjs → constants.js} +4 -3
  25. package/dist/constants.js.map +1 -0
  26. package/dist/{errors.d.cts → errors.d.ts} +2 -2
  27. package/dist/errors.d.ts.map +1 -0
  28. package/dist/{errors.mjs → errors.js} +5 -1
  29. package/dist/errors.js.map +1 -0
  30. package/dist/index.d.ts +12 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +7 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/logger.d.ts +4 -0
  35. package/dist/logger.d.ts.map +1 -0
  36. package/dist/logger.js +5 -0
  37. package/dist/logger.js.map +1 -0
  38. package/dist/selectors.d.ts +38 -0
  39. package/dist/selectors.d.ts.map +1 -0
  40. package/dist/selectors.js +46 -0
  41. package/dist/selectors.js.map +1 -0
  42. package/dist/{types.d.mts → types.d.ts} +112 -41
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/{types.mjs → types.js} +11 -1
  45. package/dist/types.js.map +1 -0
  46. package/package.json +22 -25
  47. package/dist/SubscriptionController-method-action-types.cjs +0 -7
  48. package/dist/SubscriptionController-method-action-types.cjs.map +0 -1
  49. package/dist/SubscriptionController-method-action-types.d.cts +0 -250
  50. package/dist/SubscriptionController-method-action-types.d.cts.map +0 -1
  51. package/dist/SubscriptionController-method-action-types.d.mts.map +0 -1
  52. package/dist/SubscriptionController-method-action-types.mjs.map +0 -1
  53. package/dist/SubscriptionController.cjs +0 -744
  54. package/dist/SubscriptionController.cjs.map +0 -1
  55. package/dist/SubscriptionController.d.cts +0 -258
  56. package/dist/SubscriptionController.d.cts.map +0 -1
  57. package/dist/SubscriptionController.d.mts.map +0 -1
  58. package/dist/SubscriptionController.mjs.map +0 -1
  59. package/dist/SubscriptionService-method-action-types.cjs +0 -7
  60. package/dist/SubscriptionService-method-action-types.cjs.map +0 -1
  61. package/dist/SubscriptionService-method-action-types.d.cts +0 -155
  62. package/dist/SubscriptionService-method-action-types.d.cts.map +0 -1
  63. package/dist/SubscriptionService-method-action-types.d.mts.map +0 -1
  64. package/dist/SubscriptionService-method-action-types.mjs.map +0 -1
  65. package/dist/SubscriptionService-structs.cjs +0 -158
  66. package/dist/SubscriptionService-structs.cjs.map +0 -1
  67. package/dist/SubscriptionService-structs.d.cts +0 -721
  68. package/dist/SubscriptionService-structs.d.cts.map +0 -1
  69. package/dist/SubscriptionService-structs.d.mts.map +0 -1
  70. package/dist/SubscriptionService-structs.mjs.map +0 -1
  71. package/dist/SubscriptionService.cjs +0 -477
  72. package/dist/SubscriptionService.cjs.map +0 -1
  73. package/dist/SubscriptionService.d.cts.map +0 -1
  74. package/dist/SubscriptionService.d.mts +0 -149
  75. package/dist/SubscriptionService.d.mts.map +0 -1
  76. package/dist/SubscriptionService.mjs.map +0 -1
  77. package/dist/constants.cjs +0 -73
  78. package/dist/constants.cjs.map +0 -1
  79. package/dist/constants.d.cts.map +0 -1
  80. package/dist/constants.d.mts +0 -49
  81. package/dist/constants.d.mts.map +0 -1
  82. package/dist/constants.mjs.map +0 -1
  83. package/dist/errors.cjs +0 -72
  84. package/dist/errors.cjs.map +0 -1
  85. package/dist/errors.d.cts.map +0 -1
  86. package/dist/errors.d.mts +0 -34
  87. package/dist/errors.d.mts.map +0 -1
  88. package/dist/errors.mjs.map +0 -1
  89. package/dist/index.cjs +0 -29
  90. package/dist/index.cjs.map +0 -1
  91. package/dist/index.d.cts +0 -11
  92. package/dist/index.d.cts.map +0 -1
  93. package/dist/index.d.mts +0 -11
  94. package/dist/index.d.mts.map +0 -1
  95. package/dist/index.mjs +0 -6
  96. package/dist/index.mjs.map +0 -1
  97. package/dist/logger.cjs +0 -8
  98. package/dist/logger.cjs.map +0 -1
  99. package/dist/logger.d.cts +0 -5
  100. package/dist/logger.d.cts.map +0 -1
  101. package/dist/logger.d.mts +0 -5
  102. package/dist/logger.d.mts.map +0 -1
  103. package/dist/logger.mjs +0 -5
  104. package/dist/logger.mjs.map +0 -1
  105. package/dist/types.cjs +0 -90
  106. package/dist/types.cjs.map +0 -1
  107. package/dist/types.d.cts +0 -574
  108. package/dist/types.d.cts.map +0 -1
  109. package/dist/types.d.mts.map +0 -1
  110. package/dist/types.mjs.map +0 -1
@@ -1,14 +1,11 @@
1
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
- 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");
4
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
- };
6
- 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;
7
- import { StaticIntervalPollingController } from "@metamask/polling-controller";
8
- import { TransactionType } from "@metamask/transaction-controller";
9
- import { BigNumber } from "bignumber.js";
10
- import { ACTIVE_SUBSCRIPTION_STATUSES, controllerName, DEFAULT_POLLING_INTERVAL, SubscriptionControllerErrorMessage } from "./constants.mjs";
11
- import { CRYPTO_AUTH_METHODS, PAYMENT_TYPES, PRODUCT_TYPES, SUBSCRIPTION_STATUSES } from "./types.mjs";
1
+ import { StaticIntervalPollingController } from '@metamask/polling-controller';
2
+ import { TransactionType } from '@metamask/transaction-controller';
3
+ import { BigNumber } from 'bignumber.js';
4
+ import deepEqual from 'fast-deep-equal';
5
+ import { ACTIVE_SUBSCRIPTION_STATUSES, controllerName, DEFAULT_POLLING_INTERVAL, SubscriptionControllerErrorMessage, } from './constants.js';
6
+ import { createModuleLogger, projectLogger } from './logger.js';
7
+ import { CRYPTO_AUTH_METHODS, PAYMENT_TYPES, PRODUCT_TYPES, SUBSCRIPTION_STATUSES, } from './types.js';
8
+ const log = createModuleLogger(projectLogger, controllerName);
12
9
  /**
13
10
  * Get the default state for the Subscription Controller.
14
11
  *
@@ -34,6 +31,12 @@ const subscriptionControllerMetadata = {
34
31
  includeInDebugSnapshot: false,
35
32
  usedInUi: true,
36
33
  },
34
+ productEntitlements: {
35
+ includeInStateLogs: false,
36
+ persist: true,
37
+ includeInDebugSnapshot: false,
38
+ usedInUi: true,
39
+ },
37
40
  lastSubscription: {
38
41
  includeInStateLogs: false,
39
42
  persist: true,
@@ -64,6 +67,12 @@ const subscriptionControllerMetadata = {
64
67
  includeInDebugSnapshot: true,
65
68
  usedInUi: true,
66
69
  },
70
+ benefits: {
71
+ includeInStateLogs: false,
72
+ persist: true,
73
+ includeInDebugSnapshot: false,
74
+ usedInUi: true,
75
+ },
67
76
  lastSelectedPaymentMethod: {
68
77
  includeInStateLogs: false,
69
78
  persist: true,
@@ -74,6 +83,7 @@ const subscriptionControllerMetadata = {
74
83
  const MESSENGER_EXPOSED_METHODS = [
75
84
  'getPricing',
76
85
  'getSubscriptions',
86
+ 'getBenefits',
77
87
  'getSubscriptionByProduct',
78
88
  'getSubscriptionsEligibilities',
79
89
  'cancelSubscription',
@@ -116,7 +126,6 @@ export class SubscriptionController extends StaticIntervalPollingController() {
116
126
  },
117
127
  messenger,
118
128
  });
119
- _SubscriptionController_instances.add(this);
120
129
  this.setIntervalLength(pollingInterval);
121
130
  this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
122
131
  }
@@ -133,38 +142,95 @@ export class SubscriptionController extends StaticIntervalPollingController() {
133
142
  return pricing;
134
143
  }
135
144
  async getSubscriptions() {
145
+ return await this.#getSubscriptions();
146
+ }
147
+ async #getSubscriptions({ refreshBenefits = true, } = {}) {
136
148
  const currentSubscriptions = this.state.subscriptions;
137
149
  const currentTrialedProducts = this.state.trialedProducts;
138
150
  const currentCustomerId = this.state.customerId;
139
151
  const currentLastSubscription = this.state.lastSubscription;
140
152
  const currentRewardAccountId = this.state.rewardAccountId;
141
- const { customerId: newCustomerId, subscriptions: newSubscriptions, trialedProducts: newTrialedProducts, lastSubscription: newLastSubscription, rewardAccountId: newRewardAccountId, } = await this.messenger.call('SubscriptionService:getSubscriptions');
142
- // check if the new subscriptions are different from the current subscriptions
143
- const areSubscriptionsEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_areSubscriptionsEqual).call(this, currentSubscriptions, newSubscriptions);
144
- // check if the new trialed products are different from the current trialed products
145
- const areTrialedProductsEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_areTrialedProductsEqual).call(this, currentTrialedProducts, newTrialedProducts);
146
- // check if the new last subscription is different from the current last subscription
147
- const isLastSubscriptionEqual = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_isSubscriptionEqual).call(this, currentLastSubscription, newLastSubscription);
153
+ const currentProductEntitlements = this.state.productEntitlements;
154
+ const currentSubscriptionState = {
155
+ customerId: currentCustomerId,
156
+ subscriptions: currentSubscriptions,
157
+ trialedProducts: currentTrialedProducts,
158
+ lastSubscription: currentLastSubscription,
159
+ rewardAccountId: currentRewardAccountId,
160
+ productEntitlements: currentProductEntitlements,
161
+ };
162
+ const newSubscriptionState = await this.messenger.call('SubscriptionService:getSubscriptions');
163
+ this.#updateSubscriptionStateIfChanged(currentSubscriptionState, newSubscriptionState);
164
+ await this.#refreshBenefitsIfActive(refreshBenefits);
165
+ return newSubscriptionState.subscriptions;
166
+ }
167
+ /**
168
+ * Updates the subscription state and refreshes the access token when the
169
+ * fetched subscription state differs from the current state.
170
+ *
171
+ * @param currentState - The subscription state captured before the request.
172
+ * @param newState - The subscription state returned by the service.
173
+ */
174
+ #updateSubscriptionStateIfChanged(currentState, newState) {
175
+ const { subscriptions: currentSubscriptions, trialedProducts: currentTrialedProducts, customerId: currentCustomerId, lastSubscription: currentLastSubscription, rewardAccountId: currentRewardAccountId, productEntitlements: currentProductEntitlements, } = currentState;
176
+ const { customerId: newCustomerId, subscriptions: newSubscriptions, trialedProducts: newTrialedProducts, lastSubscription: newLastSubscription, rewardAccountId: newRewardAccountId, productEntitlements: newProductEntitlements, } = newState;
177
+ const areSubscriptionsEqual = this.#areSubscriptionsEqual(currentSubscriptions, newSubscriptions);
178
+ const areTrialedProductsEqual = this.#areTrialedProductsEqual(currentTrialedProducts, newTrialedProducts);
179
+ const isLastSubscriptionEqual = this.#isSubscriptionEqual(currentLastSubscription, newLastSubscription);
148
180
  const areCustomerIdsEqual = currentCustomerId === newCustomerId;
149
181
  const areRewardAccountIdsEqual = currentRewardAccountId === newRewardAccountId;
182
+ const areProductEntitlementsEqual = this.#areProductEntitlementsEqual(currentProductEntitlements, newProductEntitlements);
150
183
  // only update the state if the subscriptions or trialed products are different
151
184
  // this prevents unnecessary state updates events, easier for the clients to handle
185
+ // Only update the state if any subscription-related field changed. This
186
+ // prevents unnecessary state change events for clients to handle.
152
187
  if (!areSubscriptionsEqual ||
153
188
  !isLastSubscriptionEqual ||
154
189
  !areTrialedProductsEqual ||
155
190
  !areCustomerIdsEqual ||
156
- !areRewardAccountIdsEqual) {
191
+ !areRewardAccountIdsEqual ||
192
+ !areProductEntitlementsEqual) {
157
193
  this.update((state) => {
158
194
  state.subscriptions = newSubscriptions;
159
195
  state.customerId = newCustomerId;
160
196
  state.trialedProducts = newTrialedProducts;
161
197
  state.lastSubscription = newLastSubscription;
162
198
  state.rewardAccountId = newRewardAccountId;
199
+ // Omitted entitlements are an empty map so selectors fail closed
200
+ // instead of keeping last-known paid-feature flags.
201
+ state.productEntitlements = newProductEntitlements ?? {};
163
202
  });
164
- // trigger access token refresh to ensure the user has the latest access token if subscription state change
203
+ // Trigger an access token refresh to ensure the user has the latest
204
+ // access token after a subscription state change.
165
205
  this.triggerAccessTokenRefresh();
166
206
  }
167
- return newSubscriptions;
207
+ }
208
+ /**
209
+ * Gets and stores the user's subscription benefits.
210
+ *
211
+ * @returns The benefits response.
212
+ * @throws If the user is not an active Money Account Plus subscriber or is
213
+ * not eligible for benefits.
214
+ */
215
+ async getBenefits() {
216
+ this.#assertIsActiveMoneyAccountPlusSubscriber();
217
+ const benefits = await this.messenger.call('SubscriptionService:getBenefits');
218
+ // The subscription may have become inactive while the benefits request
219
+ // was in flight. Re-check before persisting the response so stale
220
+ // benefits cannot be restored after entitlement ends.
221
+ this.#assertIsActiveMoneyAccountPlusSubscriber();
222
+ this.update((state) => {
223
+ state.benefits = benefits.eligible
224
+ ? {
225
+ billingPeriodId: benefits.billingPeriodId,
226
+ ...benefits.products,
227
+ }
228
+ : undefined;
229
+ });
230
+ if (!benefits.eligible) {
231
+ throw new Error(SubscriptionControllerErrorMessage.UserNotSubscribed);
232
+ }
233
+ return benefits;
168
234
  }
169
235
  /**
170
236
  * Get the subscription by product.
@@ -185,17 +251,18 @@ export class SubscriptionController extends StaticIntervalPollingController() {
185
251
  return await this.messenger.call('SubscriptionService:getSubscriptionsEligibilities', request);
186
252
  }
187
253
  async cancelSubscription(request) {
188
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
254
+ this.#assertIsUserSubscribed({ subscriptionId: request.subscriptionId });
189
255
  const cancelledSubscription = await this.messenger.call('SubscriptionService:cancelSubscription', request);
190
256
  this.update((state) => {
191
257
  state.subscriptions = state.subscriptions.map((subscription) => subscription.id === request.subscriptionId
192
258
  ? { ...subscription, ...cancelledSubscription }
193
259
  : subscription);
194
260
  });
261
+ await this.#refreshBenefitsIfActive();
195
262
  this.triggerAccessTokenRefresh();
196
263
  }
197
264
  async unCancelSubscription(request) {
198
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
265
+ this.#assertIsUserSubscribed({ subscriptionId: request.subscriptionId });
199
266
  const uncancelledSubscription = await this.messenger.call('SubscriptionService:unCancelSubscription', {
200
267
  subscriptionId: request.subscriptionId,
201
268
  });
@@ -204,6 +271,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
204
271
  ? { ...subscription, ...uncancelledSubscription }
205
272
  : subscription);
206
273
  });
274
+ await this.#refreshBenefitsIfActive();
207
275
  this.triggerAccessTokenRefresh();
208
276
  }
209
277
  /**
@@ -219,10 +287,10 @@ export class SubscriptionController extends StaticIntervalPollingController() {
219
287
  async startSubscriptionWithCard(request) {
220
288
  // get the latest subscriptions state before computing trial eligibility
221
289
  await this.getSubscriptions();
222
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
290
+ this.#assertIsUserNotSubscribed({ products: request.products });
223
291
  const response = await this.messenger.call('SubscriptionService:startSubscriptionWithCard', {
224
292
  ...request,
225
- isTrialRequested: __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, request.products, request.recurringInterval),
293
+ isTrialRequested: this.#getIsTrialRequested(request.products, request.recurringInterval),
226
294
  });
227
295
  // note: no need to trigger access token refresh after startSubscriptionWithCard request because this only return stripe checkout session url, subscription not created yet
228
296
  return response;
@@ -245,11 +313,11 @@ export class SubscriptionController extends StaticIntervalPollingController() {
245
313
  throw new Error(SubscriptionControllerErrorMessage.SubscriptionProductsEmpty);
246
314
  }
247
315
  // get the latest subscriptions state before computing trial eligibility
248
- await this.getSubscriptions();
249
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
316
+ await this.#getSubscriptions({ refreshBenefits: false });
317
+ this.#assertIsUserNotSubscribed({ products: request.products });
250
318
  const response = await this.messenger.call('SubscriptionService:startSubscriptionWithCrypto', {
251
319
  ...request,
252
- isTrialRequested: __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, request.products, request.recurringInterval),
320
+ isTrialRequested: this.#getIsTrialRequested(request.products, request.recurringInterval),
253
321
  });
254
322
  // Crypto start creates the subscription immediately (unlike card checkout).
255
323
  await this.getSubscriptions();
@@ -297,13 +365,13 @@ export class SubscriptionController extends StaticIntervalPollingController() {
297
365
  throw new Error('Last selected payment method not found');
298
366
  }
299
367
  const lastSelectedPaymentMethodForProduct = lastSelectedPaymentMethod[productType];
300
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsPaymentMethodCrypto).call(this, lastSelectedPaymentMethodForProduct);
301
- const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this, productType, lastSelectedPaymentMethodForProduct.plan);
368
+ this.#assertIsPaymentMethodCrypto(lastSelectedPaymentMethodForProduct);
369
+ const productPrice = this.#getProductPriceByProductAndPlan(productType, lastSelectedPaymentMethodForProduct.plan);
302
370
  // get the latest subscriptions state before computing trial eligibility
303
- await this.getSubscriptions();
304
- const isTrialRequested = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsTrialRequested).call(this, [productType], lastSelectedPaymentMethodForProduct.plan);
371
+ await this.#getSubscriptions({ refreshBenefits: false });
372
+ const isTrialRequested = this.#getIsTrialRequested([productType], lastSelectedPaymentMethodForProduct.plan);
305
373
  const currentSubscription = this.getSubscriptionByProduct(productType);
306
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertValidSubscriptionStateForCryptoApproval).call(this, {
374
+ this.#assertValidSubscriptionStateForCryptoApproval({
307
375
  productType,
308
376
  });
309
377
  // if subscription exists, this transaction is for changing payment method
@@ -338,7 +406,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
338
406
  await this.startSubscriptionWithCrypto(params);
339
407
  }
340
408
  // update the subscriptions state after subscription created in server
341
- await this.getSubscriptions();
409
+ await this.#getSubscriptions({ refreshBenefits: false });
342
410
  }
343
411
  /**
344
412
  * Get transaction params to create crypto approve transaction for subscription payment
@@ -363,7 +431,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
363
431
  if (!price) {
364
432
  throw new Error('Price not found');
365
433
  }
366
- const chainsPaymentInfo = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_findCryptoPaymentMethod).call(this, request.productType, CRYPTO_AUTH_METHODS.ERC20_APPROVAL);
434
+ const chainsPaymentInfo = this.#findCryptoPaymentMethod(request.productType, CRYPTO_AUTH_METHODS.ERC20_APPROVAL);
367
435
  if (!chainsPaymentInfo) {
368
436
  throw new Error('Chains payment info not found');
369
437
  }
@@ -461,17 +529,17 @@ export class SubscriptionController extends StaticIntervalPollingController() {
461
529
  if (request.products.length === 0) {
462
530
  throw new Error(SubscriptionControllerErrorMessage.SubscriptionProductsEmpty);
463
531
  }
464
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserNotSubscribed).call(this, { products: request.products });
532
+ this.#assertIsUserNotSubscribed({ products: request.products });
465
533
  const selectedPaymentMethod = this.state.lastSelectedPaymentMethod?.[request.products[0]];
466
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsPaymentMethodCrypto).call(this, selectedPaymentMethod);
534
+ this.#assertIsPaymentMethodCrypto(selectedPaymentMethod);
467
535
  const cryptoAuthMethod = selectedPaymentMethod.cryptoAuthMethod ??
468
536
  CRYPTO_AUTH_METHODS.ERC20_APPROVAL;
469
- const isEligibleForTrialedSponsorship = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getIsEligibleForTrialedSponsorship).call(this, request.chainId, request.products, cryptoAuthMethod);
537
+ const isEligibleForTrialedSponsorship = this.#getIsEligibleForTrialedSponsorship(request.chainId, request.products, cryptoAuthMethod);
470
538
  if (!isEligibleForTrialedSponsorship) {
471
539
  return false;
472
540
  }
473
541
  const { paymentTokenSymbol, plan } = selectedPaymentMethod;
474
- const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this,
542
+ const productPrice = this.#getProductPriceByProductAndPlan(
475
543
  // we only support one product at a time for now
476
544
  request.products[0], plan);
477
545
  const billingCycles = productPrice.minBillingCycles;
@@ -512,7 +580,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
512
580
  */
513
581
  async linkRewards(request) {
514
582
  // assert that the user is subscribed to the subscription
515
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_assertIsUserSubscribed).call(this, { subscriptionId: request.subscriptionId });
583
+ this.#assertIsUserSubscribed({ subscriptionId: request.subscriptionId });
516
584
  // link rewards to the subscription
517
585
  const response = await this.messenger.call('SubscriptionService:linkRewards', {
518
586
  rewardAccountId: request.rewardAccountId,
@@ -524,6 +592,54 @@ export class SubscriptionController extends StaticIntervalPollingController() {
524
592
  async _executePoll() {
525
593
  await this.getSubscriptions();
526
594
  }
595
+ async #refreshBenefitsIfActive(refreshBenefits = true) {
596
+ if (!this.#hasActiveMoneyAccountPlusSubscription()) {
597
+ if (this.state.benefits) {
598
+ this.update((state) => {
599
+ state.benefits = undefined;
600
+ });
601
+ }
602
+ return;
603
+ }
604
+ if (!refreshBenefits) {
605
+ return;
606
+ }
607
+ try {
608
+ await this.getBenefits();
609
+ }
610
+ catch (error) {
611
+ log('Failed to refresh subscription benefits', error);
612
+ }
613
+ }
614
+ /**
615
+ * Calculate total subscription price amount (approval amount) from price info
616
+ * e.g: $8 per month * 12 months min billing cycles = $96
617
+ *
618
+ * @param price - The price info
619
+ * @returns The price amount
620
+ */
621
+ #getSubscriptionPriceAmount(price) {
622
+ // no need to use BigInt since max unitDecimals are always 2 for price
623
+ const amount = new BigNumber(price.unitAmount)
624
+ .div(10 ** price.unitDecimals)
625
+ .multipliedBy(price.minBillingCycles)
626
+ .toString();
627
+ return amount;
628
+ }
629
+ /**
630
+ * Calculate minimum subscription balance amount from price info
631
+ *
632
+ * @param price - The price info
633
+ * @returns The balance amount
634
+ */
635
+ #getSubscriptionBalanceAmount(price) {
636
+ // no need to use BigInt since max unitDecimals are always 2 for price
637
+ const amount = new BigNumber(price.unitAmount)
638
+ .div(10 ** price.unitDecimals)
639
+ .multipliedBy(price.minBillingCyclesForBalance)
640
+ .toString();
641
+ return amount;
642
+ }
527
643
  /**
528
644
  * Calculate token approve amount from price info
529
645
  *
@@ -537,7 +653,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
537
653
  throw new Error('Conversion rate not found');
538
654
  }
539
655
  // price of the product
540
- const priceAmount = new BigNumber(__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getSubscriptionPriceAmount).call(this, price));
656
+ const priceAmount = new BigNumber(this.#getSubscriptionPriceAmount(price));
541
657
  const tokenDecimal = new BigNumber(10).pow(tokenPaymentInfo.decimals);
542
658
  const tokenAmount = priceAmount
543
659
  .multipliedBy(tokenDecimal)
@@ -556,7 +672,7 @@ export class SubscriptionController extends StaticIntervalPollingController() {
556
672
  if (!conversionRate) {
557
673
  throw new Error('Conversion rate not found');
558
674
  }
559
- const balanceAmount = new BigNumber(__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getSubscriptionBalanceAmount).call(this, price));
675
+ const balanceAmount = new BigNumber(this.#getSubscriptionBalanceAmount(price));
560
676
  const tokenDecimal = new BigNumber(10).pow(tokenPaymentInfo.decimals);
561
677
  const tokenAmount = balanceAmount
562
678
  .multipliedBy(tokenDecimal)
@@ -581,159 +697,244 @@ export class SubscriptionController extends StaticIntervalPollingController() {
581
697
  // will be fetched.
582
698
  this.messenger.call('AuthenticationController:performSignOut');
583
699
  }
584
- }
585
- _SubscriptionController_instances = new WeakSet(), _SubscriptionController_getSubscriptionPriceAmount = function _SubscriptionController_getSubscriptionPriceAmount(price) {
586
- // no need to use BigInt since max unitDecimals are always 2 for price
587
- const amount = new BigNumber(price.unitAmount)
588
- .div(10 ** price.unitDecimals)
589
- .multipliedBy(price.minBillingCycles)
590
- .toString();
591
- return amount;
592
- }, _SubscriptionController_getSubscriptionBalanceAmount = function _SubscriptionController_getSubscriptionBalanceAmount(price) {
593
- // no need to use BigInt since max unitDecimals are always 2 for price
594
- const amount = new BigNumber(price.unitAmount)
595
- .div(10 ** price.unitDecimals)
596
- .multipliedBy(price.minBillingCyclesForBalance)
597
- .toString();
598
- return amount;
599
- }, _SubscriptionController_getProductPriceByProductAndPlan = function _SubscriptionController_getProductPriceByProductAndPlan(productType, plan) {
600
- const { pricing } = this.state;
601
- const productPricing = pricing?.products.find((product) => product.name === productType);
602
- const productPrice = productPricing?.prices.find((price) => price.interval === plan);
603
- if (!productPrice) {
604
- throw new Error(SubscriptionControllerErrorMessage.ProductPriceNotFound);
605
- }
606
- return productPrice;
607
- }, _SubscriptionController_assertValidSubscriptionStateForCryptoApproval = function _SubscriptionController_assertValidSubscriptionStateForCryptoApproval({ productType, }) {
608
- const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => product.name === productType));
609
- const isValid = !subscription ||
610
- [
611
- SUBSCRIPTION_STATUSES.pastDue,
612
- SUBSCRIPTION_STATUSES.unpaid,
613
- SUBSCRIPTION_STATUSES.paused,
614
- SUBSCRIPTION_STATUSES.provisional,
615
- SUBSCRIPTION_STATUSES.active,
616
- SUBSCRIPTION_STATUSES.trialing,
617
- ].includes(subscription.status);
618
- if (!isValid) {
619
- throw new Error(SubscriptionControllerErrorMessage.SubscriptionNotValidForCryptoApproval);
620
- }
621
- }, _SubscriptionController_assertIsUserNotSubscribed = function _SubscriptionController_assertIsUserNotSubscribed({ products }) {
622
- const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => products.includes(product.name)));
623
- if (subscription &&
624
- ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) {
625
- throw new Error(SubscriptionControllerErrorMessage.UserAlreadySubscribed);
626
- }
627
- }, _SubscriptionController_assertIsUserSubscribed = function _SubscriptionController_assertIsUserSubscribed(request) {
628
- if (!this.state.subscriptions.find((subscription) => subscription.id === request.subscriptionId)) {
629
- throw new Error(SubscriptionControllerErrorMessage.UserNotSubscribed);
630
- }
631
- }, _SubscriptionController_assertIsPaymentMethodCrypto = function _SubscriptionController_assertIsPaymentMethodCrypto(value) {
632
- if (value?.type !== PAYMENT_TYPES.byCrypto ||
633
- !value.paymentTokenAddress ||
634
- !value.paymentTokenSymbol) {
635
- throw new Error(SubscriptionControllerErrorMessage.PaymentMethodNotCrypto);
636
- }
637
- }, _SubscriptionController_getIsEligibleForTrialedSponsorship = function _SubscriptionController_getIsEligibleForTrialedSponsorship(chainId, products, cryptoAuthMethod) {
638
- const isSponsorshipSupported = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getChainSupportsSponsorship).call(this, chainId, products[0], cryptoAuthMethod);
639
- // verify if the user has trialed the provided products before
640
- const hasTrialedBefore = this.state.trialedProducts.some((product) => products.includes(product));
641
- return isSponsorshipSupported && !hasTrialedBefore;
642
- }, _SubscriptionController_getIsTrialRequested = function _SubscriptionController_getIsTrialRequested(products, plan) {
643
- return products.every((productType) => {
644
- if (this.state.trialedProducts.includes(productType)) {
645
- return false;
700
+ #getProductPriceByProductAndPlan(productType, plan) {
701
+ const { pricing } = this.state;
702
+ const productPricing = pricing?.products.find((product) => product.name === productType);
703
+ const productPrice = productPricing?.prices.find((price) => price.interval === plan);
704
+ if (!productPrice) {
705
+ throw new Error(SubscriptionControllerErrorMessage.ProductPriceNotFound);
646
706
  }
647
- const productPrice = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_getProductPriceByProductAndPlan).call(this, productType, plan);
648
- return productPrice.trialPeriodDays > 0;
649
- });
650
- }, _SubscriptionController_findCryptoPaymentMethod = function _SubscriptionController_findCryptoPaymentMethod(productType, cryptoAuthMethod) {
651
- const matches = [];
652
- for (const paymentMethod of this.state.pricing?.paymentMethods ?? []) {
653
- if (paymentMethod.type !== PAYMENT_TYPES.byCrypto) {
654
- continue;
655
- }
656
- const resolved = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_resolveCryptoPaymentMethodDefaults).call(this, paymentMethod);
657
- if (!resolved) {
658
- continue;
659
- }
660
- if (resolved.cryptoAuthMethod === cryptoAuthMethod &&
661
- resolved.products.includes(productType)) {
662
- matches.push({
663
- method: paymentMethod,
664
- explicit: resolved.explicit,
665
- });
707
+ return productPrice;
708
+ }
709
+ #assertValidSubscriptionStateForCryptoApproval({ productType, }) {
710
+ const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => product.name === productType));
711
+ const isValid = !subscription ||
712
+ [
713
+ SUBSCRIPTION_STATUSES.pastDue,
714
+ SUBSCRIPTION_STATUSES.unpaid,
715
+ SUBSCRIPTION_STATUSES.paused,
716
+ SUBSCRIPTION_STATUSES.provisional,
717
+ SUBSCRIPTION_STATUSES.active,
718
+ SUBSCRIPTION_STATUSES.trialing,
719
+ ].includes(subscription.status);
720
+ if (!isValid) {
721
+ throw new Error(SubscriptionControllerErrorMessage.SubscriptionNotValidForCryptoApproval);
722
+ }
723
+ }
724
+ #assertIsUserNotSubscribed({ products }) {
725
+ const subscription = this.state.subscriptions.find((sub) => sub.products.some((product) => products.includes(product.name)));
726
+ if (subscription &&
727
+ ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status)) {
728
+ throw new Error(SubscriptionControllerErrorMessage.UserAlreadySubscribed);
729
+ }
730
+ }
731
+ #assertIsActiveMoneyAccountPlusSubscriber() {
732
+ if (!this.#hasActiveMoneyAccountPlusSubscription()) {
733
+ if (this.state.benefits) {
734
+ this.update((state) => {
735
+ state.benefits = undefined;
736
+ });
737
+ }
738
+ throw new Error(SubscriptionControllerErrorMessage.UserNotSubscribed);
739
+ }
740
+ }
741
+ #assertIsUserSubscribed(request) {
742
+ if (!this.state.subscriptions.find((subscription) => subscription.id === request.subscriptionId)) {
743
+ throw new Error(SubscriptionControllerErrorMessage.UserNotSubscribed);
744
+ }
745
+ }
746
+ /**
747
+ * Asserts that the value is a valid crypto payment method.
748
+ *
749
+ * After this assert, `cryptoAuthMethod` and `useTestClock` remain optional
750
+ * because persisted cache entries may omit them.
751
+ *
752
+ * @param value - The value to assert.
753
+ * @throws an error if the value is not a valid crypto payment method.
754
+ */
755
+ #assertIsPaymentMethodCrypto(value) {
756
+ if (value?.type !== PAYMENT_TYPES.byCrypto ||
757
+ !value.paymentTokenAddress ||
758
+ !value.paymentTokenSymbol) {
759
+ throw new Error(SubscriptionControllerErrorMessage.PaymentMethodNotCrypto);
666
760
  }
667
761
  }
668
- if (matches.length === 0) {
669
- return undefined;
762
+ #hasActiveMoneyAccountPlusSubscription() {
763
+ return this.state.subscriptions.some((subscription) => subscription.products.some((product) => product.name === PRODUCT_TYPES.MONEY_ACCOUNT_PLUS) && ACTIVE_SUBSCRIPTION_STATUSES.includes(subscription.status));
670
764
  }
671
- const explicitMatches = matches.filter((match) => match.explicit);
672
- const candidates = explicitMatches.length > 0 ? explicitMatches : matches;
673
- if (candidates.length > 1) {
674
- throw new Error('Multiple matching crypto payment methods found');
765
+ /**
766
+ * Determines if the user is eligible for trialed sponsorship for the given chain and products.
767
+ * The user is eligible if the chain supports sponsorship and the user has not trialed the provided products before.
768
+ *
769
+ * @param chainId - The chain ID
770
+ * @param products - The products to check eligibility for
771
+ * @param cryptoAuthMethod - The crypto authorization method of the selected payment method
772
+ * @returns True if the user is eligible for trialed sponsorship, false otherwise
773
+ */
774
+ #getIsEligibleForTrialedSponsorship(chainId, products, cryptoAuthMethod) {
775
+ const isSponsorshipSupported = this.#getChainSupportsSponsorship(chainId, products[0], cryptoAuthMethod);
776
+ // verify if the user has trialed the provided products before
777
+ const hasTrialedBefore = this.state.trialedProducts.some((product) => products.includes(product));
778
+ return isSponsorshipSupported && !hasTrialedBefore;
675
779
  }
676
- return candidates[0].method;
677
- }, _SubscriptionController_resolveCryptoPaymentMethodDefaults = function _SubscriptionController_resolveCryptoPaymentMethodDefaults(paymentMethod) {
678
- const productsPresent = paymentMethod.products !== undefined;
679
- const authPresent = paymentMethod.cryptoAuthMethod !== undefined;
680
- if (!productsPresent && !authPresent) {
780
+ /**
781
+ * Whether a trial should be requested for the given products and plan.
782
+ * True only when every product has `trialPeriodDays > 0` and has not
783
+ * already been trialed.
784
+ *
785
+ * @param products - The products to check.
786
+ * @param plan - The recurring interval to look up pricing for.
787
+ * @returns Whether a trial should be requested.
788
+ */
789
+ #getIsTrialRequested(products, plan) {
790
+ return products.every((productType) => {
791
+ if (this.state.trialedProducts.includes(productType)) {
792
+ return false;
793
+ }
794
+ const productPrice = this.#getProductPriceByProductAndPlan(productType, plan);
795
+ return productPrice.trialPeriodDays > 0;
796
+ });
797
+ }
798
+ #findCryptoPaymentMethod(productType, cryptoAuthMethod) {
799
+ const matches = [];
800
+ for (const paymentMethod of this.state.pricing?.paymentMethods ?? []) {
801
+ if (paymentMethod.type !== PAYMENT_TYPES.byCrypto) {
802
+ continue;
803
+ }
804
+ const resolved = this.#resolveCryptoPaymentMethodDefaults(paymentMethod);
805
+ if (!resolved) {
806
+ continue;
807
+ }
808
+ if (resolved.cryptoAuthMethod === cryptoAuthMethod &&
809
+ resolved.products.includes(productType)) {
810
+ matches.push({
811
+ method: paymentMethod,
812
+ explicit: resolved.explicit,
813
+ });
814
+ }
815
+ }
816
+ if (matches.length === 0) {
817
+ return undefined;
818
+ }
819
+ const explicitMatches = matches.filter((match) => match.explicit);
820
+ const candidates = explicitMatches.length > 0 ? explicitMatches : matches;
821
+ if (candidates.length > 1) {
822
+ throw new Error('Multiple matching crypto payment methods found');
823
+ }
824
+ return candidates[0].method;
825
+ }
826
+ /**
827
+ * Resolves omitted `products` / `cryptoAuthMethod` on a crypto pricing row.
828
+ * Legacy Shield + `erc20_approval` defaults apply only when both fields are
829
+ * absent. If `products` is present, the list must be non-empty and
830
+ * `cryptoAuthMethod` must be explicit; otherwise the row is ignored.
831
+ *
832
+ * @param paymentMethod - The crypto pricing row.
833
+ * @returns Resolved products and auth method, or `undefined` if the row is
834
+ * incomplete.
835
+ */
836
+ #resolveCryptoPaymentMethodDefaults(paymentMethod) {
837
+ const productsPresent = paymentMethod.products !== undefined;
838
+ const authPresent = paymentMethod.cryptoAuthMethod !== undefined;
839
+ if (!productsPresent && !authPresent) {
840
+ return {
841
+ products: [PRODUCT_TYPES.SHIELD],
842
+ cryptoAuthMethod: CRYPTO_AUTH_METHODS.ERC20_APPROVAL,
843
+ explicit: false,
844
+ };
845
+ }
846
+ if (!paymentMethod.products?.length || !paymentMethod.cryptoAuthMethod) {
847
+ return undefined;
848
+ }
681
849
  return {
682
- products: [PRODUCT_TYPES.SHIELD],
683
- cryptoAuthMethod: CRYPTO_AUTH_METHODS.ERC20_APPROVAL,
684
- explicit: false,
850
+ products: paymentMethod.products,
851
+ cryptoAuthMethod: paymentMethod.cryptoAuthMethod,
852
+ explicit: true,
685
853
  };
686
854
  }
687
- if (!paymentMethod.products?.length || !paymentMethod.cryptoAuthMethod) {
688
- return undefined;
855
+ /**
856
+ * Whether the given chain supports sponsorship for the product and auth method.
857
+ *
858
+ * @param chainId - The chain ID
859
+ * @param productType - The product type
860
+ * @param cryptoAuthMethod - The crypto authorization method to look up
861
+ * @returns True if the chain row has sponsorship enabled, false if it is explicitly not sponsored
862
+ * @throws If the crypto payment method or chain row is missing from pricing
863
+ */
864
+ #getChainSupportsSponsorship(chainId, productType, cryptoAuthMethod) {
865
+ const cryptoPaymentInfo = this.#findCryptoPaymentMethod(productType, cryptoAuthMethod);
866
+ if (!cryptoPaymentInfo) {
867
+ throw new Error('Chains payment info not found');
868
+ }
869
+ const chainPaymentInfo = cryptoPaymentInfo.chains?.find((chain) => chain.chainId === chainId);
870
+ if (!chainPaymentInfo) {
871
+ throw new Error('Invalid chain id');
872
+ }
873
+ return Boolean(chainPaymentInfo.isSponsorshipSupported);
874
+ }
875
+ /**
876
+ * Determines whether two trialed products arrays are equal by comparing all products in the arrays.
877
+ *
878
+ * @param oldTrialedProducts - The first trialed products array to compare.
879
+ * @param newTrialedProducts - The second trialed products array to compare.
880
+ * @returns True if the trialed products arrays are equal, false otherwise.
881
+ */
882
+ #areTrialedProductsEqual(oldTrialedProducts, newTrialedProducts) {
883
+ return (oldTrialedProducts.length === newTrialedProducts?.length &&
884
+ oldTrialedProducts.every((product) => newTrialedProducts?.includes(product)));
689
885
  }
690
- return {
691
- products: paymentMethod.products,
692
- cryptoAuthMethod: paymentMethod.cryptoAuthMethod,
693
- explicit: true,
694
- };
695
- }, _SubscriptionController_getChainSupportsSponsorship = function _SubscriptionController_getChainSupportsSponsorship(chainId, productType, cryptoAuthMethod) {
696
- const cryptoPaymentInfo = __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_findCryptoPaymentMethod).call(this, productType, cryptoAuthMethod);
697
- if (!cryptoPaymentInfo) {
698
- throw new Error('Chains payment info not found');
699
- }
700
- const chainPaymentInfo = cryptoPaymentInfo.chains?.find((chain) => chain.chainId === chainId);
701
- if (!chainPaymentInfo) {
702
- throw new Error('Invalid chain id');
703
- }
704
- return Boolean(chainPaymentInfo.isSponsorshipSupported);
705
- }, _SubscriptionController_areTrialedProductsEqual = function _SubscriptionController_areTrialedProductsEqual(oldTrialedProducts, newTrialedProducts) {
706
- return (oldTrialedProducts.length === newTrialedProducts?.length &&
707
- oldTrialedProducts.every((product) => newTrialedProducts?.includes(product)));
708
- }, _SubscriptionController_areSubscriptionsEqual = function _SubscriptionController_areSubscriptionsEqual(oldSubs, newSubs) {
709
- // Check if arrays have different lengths
710
- if (oldSubs.length !== newSubs.length) {
711
- return false;
712
- }
713
- // Sort both arrays by id to ensure consistent comparison
714
- const sortedOldSubs = [...oldSubs].sort((a, b) => a.id.localeCompare(b.id));
715
- const sortedNewSubs = [...newSubs].sort((a, b) => a.id.localeCompare(b.id));
716
- // Check if all subscriptions are equal
717
- return sortedOldSubs.every((oldSub, index) => {
718
- const newSub = sortedNewSubs[index];
719
- return __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_isSubscriptionEqual).call(this, oldSub, newSub);
720
- });
721
- }, _SubscriptionController_isSubscriptionEqual = function _SubscriptionController_isSubscriptionEqual(oldSub, newSub) {
722
- // not equal if one is undefined and the other is defined
723
- if (!oldSub || !newSub) {
724
- if (!oldSub && !newSub) {
725
- return true;
726
- }
727
- return false;
728
- }
729
- return (__classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_stringifySubscription).call(this, oldSub) ===
730
- __classPrivateFieldGet(this, _SubscriptionController_instances, "m", _SubscriptionController_stringifySubscription).call(this, newSub));
731
- }, _SubscriptionController_stringifySubscription = function _SubscriptionController_stringifySubscription(subscription) {
732
- const subsWithSortedProducts = {
733
- ...subscription,
734
- // order the products by name
735
- products: [...subscription.products].sort((a, b) => a.name.localeCompare(b.name)),
736
- };
737
- return JSON.stringify(subsWithSortedProducts);
738
- };
739
- //# sourceMappingURL=SubscriptionController.mjs.map
886
+ /**
887
+ * Compares product entitlement maps without treating object key order as a
888
+ * change. Insertion-order-sensitive `JSON.stringify` can otherwise treat an
889
+ * equivalent API payload as different and trigger `performSignOut`.
890
+ *
891
+ * @param oldProductEntitlements - Stored product entitlements.
892
+ * @param newProductEntitlements - Freshly fetched product entitlements.
893
+ * @returns True if the maps are equal regardless of key order.
894
+ */
895
+ #areProductEntitlementsEqual(oldProductEntitlements, newProductEntitlements) {
896
+ return deepEqual(oldProductEntitlements ?? {}, newProductEntitlements ?? {});
897
+ }
898
+ /**
899
+ * Determines whether two subscription arrays are equal by comparing all properties
900
+ * of each subscription in the arrays.
901
+ *
902
+ * @param oldSubs - The first subscription array to compare.
903
+ * @param newSubs - The second subscription array to compare.
904
+ * @returns True if the subscription arrays are equal, false otherwise.
905
+ */
906
+ #areSubscriptionsEqual(oldSubs, newSubs) {
907
+ // Check if arrays have different lengths
908
+ if (oldSubs.length !== newSubs.length) {
909
+ return false;
910
+ }
911
+ // Sort both arrays by id to ensure consistent comparison
912
+ const sortedOldSubs = [...oldSubs].sort((a, b) => a.id.localeCompare(b.id));
913
+ const sortedNewSubs = [...newSubs].sort((a, b) => a.id.localeCompare(b.id));
914
+ // Check if all subscriptions are equal
915
+ return sortedOldSubs.every((oldSub, index) => {
916
+ const newSub = sortedNewSubs[index];
917
+ return this.#isSubscriptionEqual(oldSub, newSub);
918
+ });
919
+ }
920
+ #isSubscriptionEqual(oldSub, newSub) {
921
+ // not equal if one is undefined and the other is defined
922
+ if (!oldSub || !newSub) {
923
+ if (!oldSub && !newSub) {
924
+ return true;
925
+ }
926
+ return false;
927
+ }
928
+ return (this.#stringifySubscription(oldSub) ===
929
+ this.#stringifySubscription(newSub));
930
+ }
931
+ #stringifySubscription(subscription) {
932
+ const subsWithSortedProducts = {
933
+ ...subscription,
934
+ // order the products by name
935
+ products: [...subscription.products].sort((a, b) => a.name.localeCompare(b.name)),
936
+ };
937
+ return JSON.stringify(subsWithSortedProducts);
938
+ }
939
+ }
940
+ //# sourceMappingURL=SubscriptionController.js.map