@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.
- package/CHANGELOG.md +38 -1
- package/dist/{SubscriptionController-method-action-types.d.mts → SubscriptionController-method-action-types.d.ts} +14 -3
- package/dist/SubscriptionController-method-action-types.d.ts.map +1 -0
- package/dist/{SubscriptionController-method-action-types.mjs → SubscriptionController-method-action-types.js} +1 -1
- package/dist/SubscriptionController-method-action-types.js.map +1 -0
- package/dist/{SubscriptionController.d.mts → SubscriptionController.d.ts} +29 -19
- package/dist/SubscriptionController.d.ts.map +1 -0
- package/dist/{SubscriptionController.mjs → SubscriptionController.js} +391 -190
- package/dist/SubscriptionController.js.map +1 -0
- package/dist/{SubscriptionService-method-action-types.d.mts → SubscriptionService-method-action-types.d.ts} +12 -3
- package/dist/SubscriptionService-method-action-types.d.ts.map +1 -0
- package/dist/{SubscriptionService-method-action-types.mjs → SubscriptionService-method-action-types.js} +1 -1
- package/dist/SubscriptionService-method-action-types.js.map +1 -0
- package/dist/{SubscriptionService-structs.d.mts → SubscriptionService-structs.d.ts} +302 -134
- package/dist/SubscriptionService-structs.d.ts.map +1 -0
- package/dist/{SubscriptionService-structs.mjs → SubscriptionService-structs.js} +55 -4
- package/dist/SubscriptionService-structs.js.map +1 -0
- package/dist/{SubscriptionService.d.cts → SubscriptionService.d.ts} +16 -10
- package/dist/SubscriptionService.d.ts.map +1 -0
- package/dist/{SubscriptionService.mjs → SubscriptionService.js} +159 -141
- package/dist/SubscriptionService.js.map +1 -0
- package/dist/{constants.d.cts → constants.d.ts} +2 -1
- package/dist/constants.d.ts.map +1 -0
- package/dist/{constants.mjs → constants.js} +4 -3
- package/dist/constants.js.map +1 -0
- package/dist/{errors.d.cts → errors.d.ts} +2 -2
- package/dist/errors.d.ts.map +1 -0
- package/dist/{errors.mjs → errors.js} +5 -1
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +5 -0
- package/dist/logger.js.map +1 -0
- package/dist/selectors.d.ts +38 -0
- package/dist/selectors.d.ts.map +1 -0
- package/dist/selectors.js +46 -0
- package/dist/selectors.js.map +1 -0
- package/dist/{types.d.mts → types.d.ts} +112 -41
- package/dist/types.d.ts.map +1 -0
- package/dist/{types.mjs → types.js} +11 -1
- package/dist/types.js.map +1 -0
- package/package.json +22 -25
- package/dist/SubscriptionController-method-action-types.cjs +0 -7
- package/dist/SubscriptionController-method-action-types.cjs.map +0 -1
- package/dist/SubscriptionController-method-action-types.d.cts +0 -250
- package/dist/SubscriptionController-method-action-types.d.cts.map +0 -1
- package/dist/SubscriptionController-method-action-types.d.mts.map +0 -1
- package/dist/SubscriptionController-method-action-types.mjs.map +0 -1
- package/dist/SubscriptionController.cjs +0 -744
- package/dist/SubscriptionController.cjs.map +0 -1
- package/dist/SubscriptionController.d.cts +0 -258
- package/dist/SubscriptionController.d.cts.map +0 -1
- package/dist/SubscriptionController.d.mts.map +0 -1
- package/dist/SubscriptionController.mjs.map +0 -1
- package/dist/SubscriptionService-method-action-types.cjs +0 -7
- package/dist/SubscriptionService-method-action-types.cjs.map +0 -1
- package/dist/SubscriptionService-method-action-types.d.cts +0 -155
- package/dist/SubscriptionService-method-action-types.d.cts.map +0 -1
- package/dist/SubscriptionService-method-action-types.d.mts.map +0 -1
- package/dist/SubscriptionService-method-action-types.mjs.map +0 -1
- package/dist/SubscriptionService-structs.cjs +0 -158
- package/dist/SubscriptionService-structs.cjs.map +0 -1
- package/dist/SubscriptionService-structs.d.cts +0 -721
- package/dist/SubscriptionService-structs.d.cts.map +0 -1
- package/dist/SubscriptionService-structs.d.mts.map +0 -1
- package/dist/SubscriptionService-structs.mjs.map +0 -1
- package/dist/SubscriptionService.cjs +0 -477
- package/dist/SubscriptionService.cjs.map +0 -1
- package/dist/SubscriptionService.d.cts.map +0 -1
- package/dist/SubscriptionService.d.mts +0 -149
- package/dist/SubscriptionService.d.mts.map +0 -1
- package/dist/SubscriptionService.mjs.map +0 -1
- package/dist/constants.cjs +0 -73
- package/dist/constants.cjs.map +0 -1
- package/dist/constants.d.cts.map +0 -1
- package/dist/constants.d.mts +0 -49
- package/dist/constants.d.mts.map +0 -1
- package/dist/constants.mjs.map +0 -1
- package/dist/errors.cjs +0 -72
- package/dist/errors.cjs.map +0 -1
- package/dist/errors.d.cts.map +0 -1
- package/dist/errors.d.mts +0 -34
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs.map +0 -1
- package/dist/index.cjs +0 -29
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -11
- package/dist/index.d.cts.map +0 -1
- package/dist/index.d.mts +0 -11
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -6
- package/dist/index.mjs.map +0 -1
- package/dist/logger.cjs +0 -8
- package/dist/logger.cjs.map +0 -1
- package/dist/logger.d.cts +0 -5
- package/dist/logger.d.cts.map +0 -1
- package/dist/logger.d.mts +0 -5
- package/dist/logger.d.mts.map +0 -1
- package/dist/logger.mjs +0 -5
- package/dist/logger.mjs.map +0 -1
- package/dist/types.cjs +0 -90
- package/dist/types.cjs.map +0 -1
- package/dist/types.d.cts +0 -574
- package/dist/types.d.cts.map +0 -1
- package/dist/types.d.mts.map +0 -1
- package/dist/types.mjs.map +0 -1
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
+
this.#assertIsUserNotSubscribed({ products: request.products });
|
|
223
291
|
const response = await this.messenger.call('SubscriptionService:startSubscriptionWithCard', {
|
|
224
292
|
...request,
|
|
225
|
-
isTrialRequested:
|
|
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
|
|
249
|
-
|
|
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:
|
|
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
|
-
|
|
301
|
-
const productPrice =
|
|
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
|
|
304
|
-
const isTrialRequested =
|
|
371
|
+
await this.#getSubscriptions({ refreshBenefits: false });
|
|
372
|
+
const isTrialRequested = this.#getIsTrialRequested([productType], lastSelectedPaymentMethodForProduct.plan);
|
|
305
373
|
const currentSubscription = this.getSubscriptionByProduct(productType);
|
|
306
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
532
|
+
this.#assertIsUserNotSubscribed({ products: request.products });
|
|
465
533
|
const selectedPaymentMethod = this.state.lastSelectedPaymentMethod?.[request.products[0]];
|
|
466
|
-
|
|
534
|
+
this.#assertIsPaymentMethodCrypto(selectedPaymentMethod);
|
|
467
535
|
const cryptoAuthMethod = selectedPaymentMethod.cryptoAuthMethod ??
|
|
468
536
|
CRYPTO_AUTH_METHODS.ERC20_APPROVAL;
|
|
469
|
-
const isEligibleForTrialedSponsorship =
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
if (
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
669
|
-
return
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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:
|
|
683
|
-
cryptoAuthMethod:
|
|
684
|
-
explicit:
|
|
850
|
+
products: paymentMethod.products,
|
|
851
|
+
cryptoAuthMethod: paymentMethod.cryptoAuthMethod,
|
|
852
|
+
explicit: true,
|
|
685
853
|
};
|
|
686
854
|
}
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|