@freemius/sdk 0.0.1 → 0.0.3
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/README.md +155 -0
- package/dist/index.d.mts +1082 -360
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +1082 -360
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1511 -455
- package/dist/index.mjs +1513 -456
- package/dist/index.mjs.map +1 -1
- package/package.json +10 -7
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
const crypto = __toESM(require("crypto"));
|
|
25
25
|
const openapi_fetch = __toESM(require("openapi-fetch"));
|
|
26
26
|
const __freemius_checkout = __toESM(require("@freemius/checkout"));
|
|
27
|
+
const zod = __toESM(require("zod"));
|
|
27
28
|
|
|
28
29
|
//#region src/contracts/types.ts
|
|
29
30
|
/**
|
|
@@ -107,18 +108,35 @@ function parsePaymentMethod(gateway) {
|
|
|
107
108
|
return gateway?.startsWith("stripe") ? "card" : gateway?.startsWith("paypal") ? "paypal" : null;
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
//#endregion
|
|
112
|
+
//#region package.json
|
|
113
|
+
var version = "0.0.3";
|
|
114
|
+
|
|
110
115
|
//#endregion
|
|
111
116
|
//#region src/api/client.ts
|
|
117
|
+
function detectPlatform() {
|
|
118
|
+
if (typeof globalThis !== "undefined" && "Bun" in globalThis) return "Bun";
|
|
119
|
+
if (typeof globalThis !== "undefined" && "Deno" in globalThis) return "Deno";
|
|
120
|
+
if (typeof globalThis !== "undefined" && "process" in globalThis && globalThis.process && typeof globalThis.process === "object" && "versions" in globalThis.process && globalThis.process.versions && "node" in globalThis.process.versions) return "Node";
|
|
121
|
+
if (typeof globalThis !== "undefined" && "window" in globalThis) return "Browser";
|
|
122
|
+
return "Unknown";
|
|
123
|
+
}
|
|
112
124
|
function createApiClient(baseUrl, bearerToken) {
|
|
113
|
-
|
|
125
|
+
const platform = detectPlatform();
|
|
126
|
+
const client = (0, openapi_fetch.default)({
|
|
114
127
|
baseUrl,
|
|
115
|
-
headers: {
|
|
128
|
+
headers: {
|
|
129
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
130
|
+
"User-Agent": `Freemius-JS-SDK/${version} (${platform})`
|
|
131
|
+
}
|
|
116
132
|
});
|
|
133
|
+
return client;
|
|
117
134
|
}
|
|
118
135
|
|
|
119
136
|
//#endregion
|
|
120
137
|
//#region src/api/ApiBase.ts
|
|
121
|
-
const
|
|
138
|
+
const PAGING_MAX_LIMIT = 50;
|
|
139
|
+
const PAGING_DEFAULT_LIMIT = PAGING_MAX_LIMIT;
|
|
122
140
|
const defaultPagingOptions = {
|
|
123
141
|
count: PAGING_DEFAULT_LIMIT,
|
|
124
142
|
offset: 0
|
|
@@ -165,6 +183,15 @@ var ApiBase = class {
|
|
|
165
183
|
isGoodResponse(response) {
|
|
166
184
|
return response.status >= 200 && response.status < 300;
|
|
167
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* @note - We must use this serializer when sending arrays as query parameter to our API.
|
|
188
|
+
*/
|
|
189
|
+
getQuerySerializerForArray() {
|
|
190
|
+
return { array: {
|
|
191
|
+
explode: false,
|
|
192
|
+
style: "form"
|
|
193
|
+
} };
|
|
194
|
+
}
|
|
168
195
|
};
|
|
169
196
|
|
|
170
197
|
//#endregion
|
|
@@ -215,7 +242,7 @@ var License = class extends ApiBase {
|
|
|
215
242
|
var Product = class extends ApiBase {
|
|
216
243
|
async retrieve() {
|
|
217
244
|
const response = await this.client.GET(`/products/{product_id}.json`, { params: { path: { product_id: this.productId } } });
|
|
218
|
-
if (response.response
|
|
245
|
+
if (!this.isGoodResponse(response.response) || !response.data) return null;
|
|
219
246
|
return response.data;
|
|
220
247
|
}
|
|
221
248
|
async retrieveMany() {
|
|
@@ -223,32 +250,71 @@ var Product = class extends ApiBase {
|
|
|
223
250
|
}
|
|
224
251
|
async retrievePricingData() {
|
|
225
252
|
const response = await this.client.GET(`/products/{product_id}/pricing.json`, { params: { path: { product_id: this.productId } } });
|
|
226
|
-
if (response.response
|
|
253
|
+
if (!this.isGoodResponse(response.response) || !response.data) return null;
|
|
227
254
|
return response.data;
|
|
228
255
|
}
|
|
256
|
+
async retrieveSubscriptionCancellationCoupon() {
|
|
257
|
+
const response = await this.client.GET(`/products/{product_id}/coupons/special.json`, { params: {
|
|
258
|
+
path: { product_id: this.productId },
|
|
259
|
+
query: { type: "subscription_cancellation" }
|
|
260
|
+
} });
|
|
261
|
+
if (!this.isGoodResponse(response.response) || !response.data || !response.data.coupon_entities) return null;
|
|
262
|
+
return response.data.coupon_entities;
|
|
263
|
+
}
|
|
229
264
|
};
|
|
230
265
|
|
|
231
266
|
//#endregion
|
|
232
267
|
//#region src/api/Subscription.ts
|
|
233
268
|
var Subscription = class extends ApiBase {
|
|
234
269
|
async retrieve(subscriptionId) {
|
|
235
|
-
const
|
|
270
|
+
const result = await this.client.GET(`/products/{product_id}/subscriptions/{subscription_id}.json`, { params: { path: {
|
|
236
271
|
product_id: this.productId,
|
|
237
272
|
subscription_id: this.getIdForPath(subscriptionId)
|
|
238
273
|
} } });
|
|
239
|
-
if (
|
|
240
|
-
return
|
|
274
|
+
if (!this.isGoodResponse(result.response) || !result.data || !result.data.id) return null;
|
|
275
|
+
return result.data;
|
|
241
276
|
}
|
|
242
277
|
async retrieveMany(filter, pagination) {
|
|
243
|
-
const
|
|
278
|
+
const result = await this.client.GET(`/products/{product_id}/subscriptions.json`, { params: {
|
|
244
279
|
path: { product_id: this.productId },
|
|
245
280
|
query: {
|
|
246
281
|
...this.getPagingParams(pagination),
|
|
247
282
|
...filter ?? {}
|
|
248
283
|
}
|
|
249
284
|
} });
|
|
250
|
-
if (
|
|
251
|
-
return
|
|
285
|
+
if (!this.isGoodResponse(result.response) || !result.data || !Array.isArray(result.data.subscriptions)) return [];
|
|
286
|
+
return result.data.subscriptions;
|
|
287
|
+
}
|
|
288
|
+
async applyRenewalCoupon(subscriptionId, couponId, logAutoRenew) {
|
|
289
|
+
const result = await this.client.PUT(`/products/{product_id}/subscriptions/{subscription_id}.json`, {
|
|
290
|
+
params: { path: {
|
|
291
|
+
product_id: this.productId,
|
|
292
|
+
subscription_id: this.getIdForPath(subscriptionId)
|
|
293
|
+
} },
|
|
294
|
+
body: {
|
|
295
|
+
auto_renew: logAutoRenew,
|
|
296
|
+
coupon_id: Number.parseInt(couponId, 10)
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
if (!this.isGoodResponse(result.response) || !result.data || !result.data.id) return null;
|
|
300
|
+
return result.data;
|
|
301
|
+
}
|
|
302
|
+
async cancel(subscriptionId, feedback, reasonIds) {
|
|
303
|
+
const result = await this.client.DELETE(`/products/{product_id}/subscriptions/{subscription_id}.json`, {
|
|
304
|
+
params: {
|
|
305
|
+
path: {
|
|
306
|
+
product_id: this.productId,
|
|
307
|
+
subscription_id: this.getIdForPath(subscriptionId)
|
|
308
|
+
},
|
|
309
|
+
query: {
|
|
310
|
+
reason: feedback,
|
|
311
|
+
reason_ids: reasonIds ?? []
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
querySerializer: this.getQuerySerializerForArray()
|
|
315
|
+
});
|
|
316
|
+
if (!this.isGoodResponse(result.response) || !result.data || !result.data.id) return null;
|
|
317
|
+
return result.data;
|
|
252
318
|
}
|
|
253
319
|
};
|
|
254
320
|
|
|
@@ -296,7 +362,7 @@ var User = class extends ApiBase {
|
|
|
296
362
|
return billingResponse.data;
|
|
297
363
|
}
|
|
298
364
|
async retrieveSubscriptions(userId, filters, pagination) {
|
|
299
|
-
const
|
|
365
|
+
const result = await this.client.GET(`/products/{product_id}/users/{user_id}/subscriptions.json`, { params: {
|
|
300
366
|
path: {
|
|
301
367
|
product_id: this.productId,
|
|
302
368
|
user_id: this.getIdForPath(userId)
|
|
@@ -306,8 +372,15 @@ var User = class extends ApiBase {
|
|
|
306
372
|
...this.getPagingParams(pagination)
|
|
307
373
|
}
|
|
308
374
|
} });
|
|
309
|
-
if (
|
|
310
|
-
|
|
375
|
+
if (!this.isGoodResponse(result.response) || !result.data || !Array.isArray(result.data.subscriptions)) return [];
|
|
376
|
+
const discountsMap = /* @__PURE__ */ new Map();
|
|
377
|
+
if (result.data.discounts) Object.entries(result.data.discounts).forEach(([subscriptionId, discounts]) => {
|
|
378
|
+
discountsMap.set(idToString(subscriptionId), discounts);
|
|
379
|
+
});
|
|
380
|
+
return result.data.subscriptions.map((subscription) => ({
|
|
381
|
+
...subscription,
|
|
382
|
+
discounts: discountsMap.get(idToString(subscription.id)) || []
|
|
383
|
+
}));
|
|
311
384
|
}
|
|
312
385
|
async retrieveLicenses(userId, filters, pagination) {
|
|
313
386
|
const response = await this.client.GET(`/products/{product_id}/users/{user_id}/licenses.json`, { params: {
|
|
@@ -337,12 +410,74 @@ var User = class extends ApiBase {
|
|
|
337
410
|
if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.payments)) return [];
|
|
338
411
|
return response.data.payments;
|
|
339
412
|
}
|
|
413
|
+
async updateBilling(userId, payload) {
|
|
414
|
+
const response = await this.client.PUT(`/products/{product_id}/users/{user_id}/billing.json`, {
|
|
415
|
+
params: { path: {
|
|
416
|
+
product_id: this.productId,
|
|
417
|
+
user_id: this.getIdForPath(userId)
|
|
418
|
+
} },
|
|
419
|
+
body: payload
|
|
420
|
+
});
|
|
421
|
+
if (!this.isGoodResponse(response.response) || !response.data || !response.data) return null;
|
|
422
|
+
return response.data;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/api/Payment.ts
|
|
428
|
+
var Payment = class extends ApiBase {
|
|
429
|
+
async retrieve(paymentId) {
|
|
430
|
+
const response = await this.client.GET(`/products/{product_id}/payments/{payment_id}.json`, { params: { path: {
|
|
431
|
+
product_id: this.productId,
|
|
432
|
+
payment_id: this.getIdForPath(paymentId)
|
|
433
|
+
} } });
|
|
434
|
+
if (!this.isGoodResponse(response.response) || !response.data || !response.data.id) return null;
|
|
435
|
+
return response.data;
|
|
436
|
+
}
|
|
437
|
+
async retrieveMany(filter, pagination) {
|
|
438
|
+
const response = await this.client.GET(`/products/{product_id}/payments.json`, { params: {
|
|
439
|
+
path: { product_id: this.productId },
|
|
440
|
+
query: {
|
|
441
|
+
...this.getPagingParams(pagination),
|
|
442
|
+
...filter ?? {}
|
|
443
|
+
}
|
|
444
|
+
} });
|
|
445
|
+
if (!this.isGoodResponse(response.response) || !response.data || !Array.isArray(response.data.payments)) return [];
|
|
446
|
+
return response.data.payments;
|
|
447
|
+
}
|
|
448
|
+
async retrieveInvoice(paymentId) {
|
|
449
|
+
const response = await this.client.GET(`/products/{product_id}/payments/{payment_id}/invoice.pdf`, {
|
|
450
|
+
params: { path: {
|
|
451
|
+
payment_id: this.getIdForPath(paymentId),
|
|
452
|
+
product_id: this.productId
|
|
453
|
+
} },
|
|
454
|
+
parseAs: "blob"
|
|
455
|
+
});
|
|
456
|
+
if (!this.isGoodResponse(response.response) || !response.data) return null;
|
|
457
|
+
return response.data;
|
|
458
|
+
}
|
|
340
459
|
};
|
|
341
460
|
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/utils/ops.ts
|
|
463
|
+
function splitName(name) {
|
|
464
|
+
const parts = name.split(" ");
|
|
465
|
+
return {
|
|
466
|
+
firstName: parts[0] ?? "",
|
|
467
|
+
lastName: parts.slice(1).join(" ") ?? ""
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function isTestServer() {
|
|
471
|
+
return process.env.FREEMIUS_INTERNAL__IS_DEVELOPMENT_MODE === "true";
|
|
472
|
+
}
|
|
473
|
+
|
|
342
474
|
//#endregion
|
|
343
475
|
//#region src/services/ApiService.ts
|
|
344
476
|
const API_ENDPOINT_PRODUCTION = "https://api.freemius.com/v1/";
|
|
345
477
|
const API_ENDPOINT_TEST = "http://api.freemius-local.com:8080/v1/";
|
|
478
|
+
/**
|
|
479
|
+
* @todo - Add a proper user-agent string with SDK version.
|
|
480
|
+
*/
|
|
346
481
|
var ApiService = class {
|
|
347
482
|
client;
|
|
348
483
|
productId;
|
|
@@ -350,18 +485,19 @@ var ApiService = class {
|
|
|
350
485
|
license;
|
|
351
486
|
product;
|
|
352
487
|
subscription;
|
|
488
|
+
payment;
|
|
353
489
|
baseUrl;
|
|
354
490
|
constructor(productId, apiKey, secretKey, publicKey) {
|
|
355
491
|
this.secretKey = secretKey;
|
|
356
492
|
this.publicKey = publicKey;
|
|
357
|
-
|
|
358
|
-
this.baseUrl = isTestServer ? API_ENDPOINT_TEST : API_ENDPOINT_PRODUCTION;
|
|
493
|
+
this.baseUrl = isTestServer() ? API_ENDPOINT_TEST : API_ENDPOINT_PRODUCTION;
|
|
359
494
|
this.client = createApiClient(this.baseUrl, apiKey);
|
|
360
495
|
this.productId = idToString(productId);
|
|
361
496
|
this.user = new User(this.productId, this.client);
|
|
362
497
|
this.license = new License(this.productId, this.client);
|
|
363
498
|
this.product = new Product(this.productId, this.client);
|
|
364
499
|
this.subscription = new Subscription(this.productId, this.client);
|
|
500
|
+
this.payment = new Payment(this.productId, this.client);
|
|
365
501
|
}
|
|
366
502
|
/**
|
|
367
503
|
* Low level API client for direct access to the Freemius API.
|
|
@@ -372,12 +508,15 @@ var ApiService = class {
|
|
|
372
508
|
get __unstable_ApiClient() {
|
|
373
509
|
return this.client;
|
|
374
510
|
}
|
|
511
|
+
createSignedUrl(path) {
|
|
512
|
+
return this.getSignedUrl(this.createUrl(path));
|
|
513
|
+
}
|
|
375
514
|
createUrl(path) {
|
|
376
515
|
path = path.replace(/^\/+/, "");
|
|
377
516
|
return `${this.baseUrl}products/${this.productId}/${path}`;
|
|
378
517
|
}
|
|
379
518
|
/**
|
|
380
|
-
* Generate signed URL for the given full URL
|
|
519
|
+
* Generate signed URL for the given full URL. The authentication is valid for 15 minutes only.
|
|
381
520
|
*/
|
|
382
521
|
getSignedUrl(fullUrl) {
|
|
383
522
|
const url = new URL(fullUrl);
|
|
@@ -431,49 +570,41 @@ var ApiService = class {
|
|
|
431
570
|
};
|
|
432
571
|
|
|
433
572
|
//#endregion
|
|
434
|
-
//#region src/
|
|
435
|
-
function splitName(name) {
|
|
436
|
-
const parts = name.split(" ");
|
|
437
|
-
return {
|
|
438
|
-
firstName: parts[0] ?? "",
|
|
439
|
-
lastName: parts.slice(1).join(" ") ?? ""
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
//#endregion
|
|
444
|
-
//#region src/models/CheckoutBuilder.ts
|
|
573
|
+
//#region src/checkout/Checkout.ts
|
|
445
574
|
/**
|
|
446
575
|
* A builder class for constructing checkout parameters. This class provides a fluent
|
|
447
576
|
* API to create Checkout parameters for a product with various configurations.
|
|
448
577
|
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* Every method returns a new instance of the builder with the updated options,
|
|
452
|
-
* allowing for method chaining. The final `toOptions()` method returns the constructed
|
|
453
|
-
* `CheckoutOptions` object. So the class itself is immutable and does not modify the original instance.
|
|
578
|
+
* Every method returns the existing instance of the builder for chainability,
|
|
579
|
+
* The final `getOptions()` method returns the constructed `CheckoutOptions` object.
|
|
454
580
|
*/
|
|
455
|
-
var
|
|
456
|
-
|
|
457
|
-
|
|
581
|
+
var Checkout = class Checkout {
|
|
582
|
+
static createSandboxToken(productId, secretKey, publicKey) {
|
|
583
|
+
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
584
|
+
const token = `${timestamp}${productId}${secretKey}${publicKey}checkout`;
|
|
585
|
+
return {
|
|
586
|
+
ctx: timestamp,
|
|
587
|
+
token: (0, crypto.createHash)("md5").update(token).digest("hex")
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
options;
|
|
591
|
+
constructor(productId, publicKey, secretKey) {
|
|
458
592
|
this.productId = productId;
|
|
459
593
|
this.publicKey = publicKey;
|
|
460
594
|
this.secretKey = secretKey;
|
|
595
|
+
this.options = { product_id: productId };
|
|
461
596
|
}
|
|
462
597
|
/**
|
|
463
598
|
* Enables sandbox mode for testing purposes.
|
|
464
599
|
*
|
|
465
600
|
* @returns A new builder instance with sandbox configuration
|
|
466
601
|
*/
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const token = `${timestamp}${this.productId}${this.secretKey}${this.publicKey}checkout`;
|
|
470
|
-
return new CheckoutBuilder({
|
|
602
|
+
setSandbox() {
|
|
603
|
+
this.options = {
|
|
471
604
|
...this.options,
|
|
472
|
-
sandbox:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
}, this.productId, this.publicKey, this.secretKey);
|
|
605
|
+
sandbox: Checkout.createSandboxToken(this.productId, this.secretKey, this.publicKey)
|
|
606
|
+
};
|
|
607
|
+
return this;
|
|
477
608
|
}
|
|
478
609
|
/**
|
|
479
610
|
* Sets user information for the checkout session.
|
|
@@ -483,7 +614,7 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
483
614
|
*
|
|
484
615
|
* @returns A new builder instance with user configuration
|
|
485
616
|
*/
|
|
486
|
-
|
|
617
|
+
setUser(user, readonly = true) {
|
|
487
618
|
if (!user) return this;
|
|
488
619
|
let firstName = user.firstName ?? "";
|
|
489
620
|
let lastName = user.lastName ?? "";
|
|
@@ -492,13 +623,14 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
492
623
|
firstName = fn;
|
|
493
624
|
lastName = ln;
|
|
494
625
|
}
|
|
495
|
-
|
|
626
|
+
this.options = {
|
|
496
627
|
...this.options,
|
|
497
628
|
user_email: user.email,
|
|
498
|
-
|
|
499
|
-
|
|
629
|
+
user_firstname: firstName,
|
|
630
|
+
user_lastname: lastName,
|
|
500
631
|
readonly_user: readonly
|
|
501
|
-
}
|
|
632
|
+
};
|
|
633
|
+
return this;
|
|
502
634
|
}
|
|
503
635
|
/**
|
|
504
636
|
* Applies recommended UI settings for better user experience.
|
|
@@ -506,15 +638,16 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
506
638
|
*
|
|
507
639
|
* @returns A new builder instance with recommended UI settings
|
|
508
640
|
*/
|
|
509
|
-
|
|
510
|
-
|
|
641
|
+
setRecommendations() {
|
|
642
|
+
this.options = {
|
|
511
643
|
...this.options,
|
|
512
644
|
fullscreen: true,
|
|
513
645
|
show_refund_badge: true,
|
|
514
646
|
show_reviews: true,
|
|
515
647
|
locale: "auto",
|
|
516
648
|
currency: "auto"
|
|
517
|
-
}
|
|
649
|
+
};
|
|
650
|
+
return this;
|
|
518
651
|
}
|
|
519
652
|
/**
|
|
520
653
|
* Sets the plan ID for the checkout.
|
|
@@ -522,11 +655,12 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
522
655
|
* @param planId The plan ID to purchase
|
|
523
656
|
* @returns A new builder instance with plan ID set
|
|
524
657
|
*/
|
|
525
|
-
|
|
526
|
-
|
|
658
|
+
setPlan(planId) {
|
|
659
|
+
this.options = {
|
|
527
660
|
...this.options,
|
|
528
661
|
plan_id: planId.toString()
|
|
529
|
-
}
|
|
662
|
+
};
|
|
663
|
+
return this;
|
|
530
664
|
}
|
|
531
665
|
/**
|
|
532
666
|
* Sets the number of licenses to purchase.
|
|
@@ -534,23 +668,26 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
534
668
|
* @param count Number of licenses
|
|
535
669
|
* @returns A new builder instance with license count set
|
|
536
670
|
*/
|
|
537
|
-
|
|
538
|
-
|
|
671
|
+
setQuota(count) {
|
|
672
|
+
this.options = {
|
|
539
673
|
...this.options,
|
|
540
674
|
licenses: count
|
|
541
|
-
}
|
|
675
|
+
};
|
|
676
|
+
return this;
|
|
542
677
|
}
|
|
543
|
-
|
|
544
|
-
|
|
678
|
+
setPricing(pricingId) {
|
|
679
|
+
this.options = {
|
|
545
680
|
...this.options,
|
|
546
681
|
pricing_id: pricingId.toString()
|
|
547
|
-
}
|
|
682
|
+
};
|
|
683
|
+
return this;
|
|
548
684
|
}
|
|
549
|
-
|
|
550
|
-
|
|
685
|
+
setTitle(title) {
|
|
686
|
+
this.options = {
|
|
551
687
|
...this.options,
|
|
552
688
|
title
|
|
553
|
-
}
|
|
689
|
+
};
|
|
690
|
+
return this;
|
|
554
691
|
}
|
|
555
692
|
/**
|
|
556
693
|
* Sets a coupon code for the checkout.
|
|
@@ -559,14 +696,14 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
559
696
|
* @param hideUI Whether to hide the coupon input field from users
|
|
560
697
|
* @returns A new builder instance with coupon configuration
|
|
561
698
|
*/
|
|
562
|
-
|
|
699
|
+
setCoupon(options) {
|
|
563
700
|
const { code: coupon, hideUI = false } = options;
|
|
564
|
-
|
|
701
|
+
this.options = {
|
|
565
702
|
...this.options,
|
|
703
|
+
coupon,
|
|
566
704
|
hide_coupon: hideUI
|
|
567
705
|
};
|
|
568
|
-
|
|
569
|
-
return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
|
|
706
|
+
return this;
|
|
570
707
|
}
|
|
571
708
|
/**
|
|
572
709
|
* Enables trial mode for the checkout.
|
|
@@ -574,11 +711,12 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
574
711
|
* @param mode Trial type - true/false for plan default, or specific 'free'/'paid' mode
|
|
575
712
|
* @returns A new builder instance with trial configuration
|
|
576
713
|
*/
|
|
577
|
-
|
|
578
|
-
|
|
714
|
+
setTrial(mode = true) {
|
|
715
|
+
this.options = {
|
|
579
716
|
...this.options,
|
|
580
717
|
trial: mode
|
|
581
|
-
}
|
|
718
|
+
};
|
|
719
|
+
return this;
|
|
582
720
|
}
|
|
583
721
|
/**
|
|
584
722
|
* Configures the visual layout and appearance of the checkout.
|
|
@@ -586,14 +724,14 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
586
724
|
* @param options Appearance configuration options
|
|
587
725
|
* @returns A new builder instance with appearance configuration
|
|
588
726
|
*/
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (options.layout !== void 0)
|
|
592
|
-
if (options.formPosition !== void 0)
|
|
593
|
-
if (options.fullscreen !== void 0)
|
|
594
|
-
if (options.modalTitle !== void 0)
|
|
595
|
-
if (options.id !== void 0)
|
|
596
|
-
return
|
|
727
|
+
setAppearance(options) {
|
|
728
|
+
this.options = { ...this.options };
|
|
729
|
+
if (options.layout !== void 0) this.options.layout = options.layout;
|
|
730
|
+
if (options.formPosition !== void 0) this.options.form_position = options.formPosition;
|
|
731
|
+
if (options.fullscreen !== void 0) this.options.fullscreen = options.fullscreen;
|
|
732
|
+
if (options.modalTitle !== void 0) this.options.modal_title = options.modalTitle;
|
|
733
|
+
if (options.id !== void 0) this.options.id = options.id;
|
|
734
|
+
return this;
|
|
597
735
|
}
|
|
598
736
|
/**
|
|
599
737
|
* Configures discount display settings.
|
|
@@ -601,13 +739,13 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
601
739
|
* @param options Discount configuration options
|
|
602
740
|
* @returns A new builder instance with discount configuration
|
|
603
741
|
*/
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
if (options.annual !== void 0)
|
|
607
|
-
if (options.multisite !== void 0)
|
|
608
|
-
if (options.bundle !== void 0)
|
|
609
|
-
if (options.showMonthlySwitch !== void 0)
|
|
610
|
-
return
|
|
742
|
+
setDiscounts(options) {
|
|
743
|
+
this.options = { ...this.options };
|
|
744
|
+
if (options.annual !== void 0) this.options.annual_discount = options.annual;
|
|
745
|
+
if (options.multisite !== void 0) this.options.multisite_discount = options.multisite;
|
|
746
|
+
if (options.bundle !== void 0) this.options.bundle_discount = options.bundle;
|
|
747
|
+
if (options.showMonthlySwitch !== void 0) this.options.show_monthly_switch = options.showMonthlySwitch;
|
|
748
|
+
return this;
|
|
611
749
|
}
|
|
612
750
|
/**
|
|
613
751
|
* Configures billing cycle selector interface.
|
|
@@ -616,11 +754,11 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
616
754
|
* @param defaultCycle Default billing cycle to select
|
|
617
755
|
* @returns A new builder instance with billing cycle configuration
|
|
618
756
|
*/
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (selector !== void 0)
|
|
622
|
-
if (defaultCycle !== void 0)
|
|
623
|
-
return
|
|
757
|
+
setBillingCycle(defaultCycle, selector) {
|
|
758
|
+
this.options = { ...this.options };
|
|
759
|
+
if (selector !== void 0) this.options.billing_cycle_selector = selector;
|
|
760
|
+
if (defaultCycle !== void 0) this.options.billing_cycle = defaultCycle;
|
|
761
|
+
return this;
|
|
624
762
|
}
|
|
625
763
|
/**
|
|
626
764
|
* Sets the language/locale for the checkout.
|
|
@@ -628,12 +766,12 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
628
766
|
* @param locale Language setting - 'auto', 'auto-beta', or specific locale like 'en_US'
|
|
629
767
|
* @returns A new builder instance with locale configuration
|
|
630
768
|
*/
|
|
631
|
-
|
|
632
|
-
|
|
769
|
+
setLanguage(locale = "auto") {
|
|
770
|
+
this.options = {
|
|
633
771
|
...this.options,
|
|
634
|
-
language: locale
|
|
635
|
-
|
|
636
|
-
|
|
772
|
+
language: locale
|
|
773
|
+
};
|
|
774
|
+
return this;
|
|
637
775
|
}
|
|
638
776
|
/**
|
|
639
777
|
* Configures review and badge display settings.
|
|
@@ -641,13 +779,13 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
641
779
|
* @param options Review and badge configuration
|
|
642
780
|
* @returns A new builder instance with reviews and badges configuration
|
|
643
781
|
*/
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
if (options.showReviews !== void 0)
|
|
647
|
-
if (options.reviewId !== void 0)
|
|
648
|
-
if (options.showRefundBadge !== void 0)
|
|
649
|
-
if (options.refundPolicyPosition !== void 0)
|
|
650
|
-
return
|
|
782
|
+
setSocialProofing(options) {
|
|
783
|
+
this.options = { ...this.options };
|
|
784
|
+
if (options.showReviews !== void 0) this.options.show_reviews = options.showReviews;
|
|
785
|
+
if (options.reviewId !== void 0) this.options.review_id = options.reviewId;
|
|
786
|
+
if (options.showRefundBadge !== void 0) this.options.show_refund_badge = options.showRefundBadge;
|
|
787
|
+
if (options.refundPolicyPosition !== void 0) this.options.refund_policy_position = options.refundPolicyPosition;
|
|
788
|
+
return this;
|
|
651
789
|
}
|
|
652
790
|
/**
|
|
653
791
|
* Enhanced currency configuration.
|
|
@@ -657,14 +795,14 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
657
795
|
* @param showInlineSelector Whether to show inline currency selector
|
|
658
796
|
* @returns A new builder instance with currency configuration
|
|
659
797
|
*/
|
|
660
|
-
|
|
661
|
-
|
|
798
|
+
setCurrency(currency, defaultCurrency = "usd", showInlineSelector = true) {
|
|
799
|
+
this.options = {
|
|
662
800
|
...this.options,
|
|
663
801
|
show_inline_currency_selector: showInlineSelector,
|
|
664
|
-
default_currency: defaultCurrency
|
|
802
|
+
default_currency: defaultCurrency,
|
|
803
|
+
currency
|
|
665
804
|
};
|
|
666
|
-
|
|
667
|
-
return new CheckoutBuilder(options, this.productId, this.publicKey, this.secretKey);
|
|
805
|
+
return this;
|
|
668
806
|
}
|
|
669
807
|
/**
|
|
670
808
|
* Configures navigation and cancel behavior.
|
|
@@ -673,11 +811,11 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
673
811
|
* @param cancelIcon Custom cancel icon URL
|
|
674
812
|
* @returns A new builder instance with navigation configuration
|
|
675
813
|
*/
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (cancelUrl !== void 0)
|
|
679
|
-
if (cancelIcon !== void 0)
|
|
680
|
-
return
|
|
814
|
+
setCancelButton(cancelUrl, cancelIcon) {
|
|
815
|
+
this.options = { ...this.options };
|
|
816
|
+
if (cancelUrl !== void 0) this.options.cancel_url = cancelUrl;
|
|
817
|
+
if (cancelIcon !== void 0) this.options.cancel_icon = cancelIcon;
|
|
818
|
+
return this;
|
|
681
819
|
}
|
|
682
820
|
/**
|
|
683
821
|
* Associates purchases with an affiliate account.
|
|
@@ -685,11 +823,12 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
685
823
|
* @param userId Affiliate user ID
|
|
686
824
|
* @returns A new builder instance with affiliate configuration
|
|
687
825
|
*/
|
|
688
|
-
|
|
689
|
-
|
|
826
|
+
setAffiliate(userId) {
|
|
827
|
+
this.options = {
|
|
690
828
|
...this.options,
|
|
691
829
|
affiliate_user_id: userId
|
|
692
|
-
}
|
|
830
|
+
};
|
|
831
|
+
return this;
|
|
693
832
|
}
|
|
694
833
|
/**
|
|
695
834
|
* Sets a custom image/icon for the checkout.
|
|
@@ -697,11 +836,12 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
697
836
|
* @param imageUrl Secure HTTPS URL to the image
|
|
698
837
|
* @returns A new builder instance with custom image
|
|
699
838
|
*/
|
|
700
|
-
|
|
701
|
-
|
|
839
|
+
setImage(imageUrl) {
|
|
840
|
+
this.options = {
|
|
702
841
|
...this.options,
|
|
703
842
|
image: imageUrl
|
|
704
|
-
}
|
|
843
|
+
};
|
|
844
|
+
return this;
|
|
705
845
|
}
|
|
706
846
|
/**
|
|
707
847
|
* Configures the checkout for license renewal.
|
|
@@ -709,30 +849,131 @@ var CheckoutBuilder = class CheckoutBuilder {
|
|
|
709
849
|
* @param licenseKey The license key to renew
|
|
710
850
|
* @returns A new builder instance configured for renewal
|
|
711
851
|
*/
|
|
712
|
-
|
|
713
|
-
|
|
852
|
+
setLicenseRenewal(licenseKey) {
|
|
853
|
+
this.options = {
|
|
714
854
|
...this.options,
|
|
715
855
|
license_key: licenseKey
|
|
716
|
-
}
|
|
856
|
+
};
|
|
857
|
+
return this;
|
|
717
858
|
}
|
|
718
859
|
/**
|
|
719
860
|
* Builds and returns the final checkout options to be used with the `@freemius/checkout` package.
|
|
720
861
|
*
|
|
862
|
+
* @note - This is async by purpose so that we can allow for future enhancements that might require async operations.
|
|
863
|
+
*
|
|
721
864
|
* @returns The constructed CheckoutOptions object
|
|
722
865
|
*/
|
|
723
|
-
|
|
724
|
-
return {
|
|
725
|
-
...this.options,
|
|
726
|
-
...additionalOptions,
|
|
727
|
-
product_id: this.productId
|
|
728
|
-
};
|
|
866
|
+
getOptions() {
|
|
867
|
+
return { ...this.options };
|
|
729
868
|
}
|
|
730
|
-
|
|
869
|
+
/**
|
|
870
|
+
* Generates a checkout link based on the current builder state.
|
|
871
|
+
*
|
|
872
|
+
* @note - This is async by purpose so that we can allow for future enhancements that might require async operations.
|
|
873
|
+
*/
|
|
874
|
+
getLink() {
|
|
731
875
|
const checkoutOptions = (0, __freemius_checkout.convertCheckoutOptionsToQueryParams)(this.options);
|
|
732
876
|
const queryParams = (0, __freemius_checkout.buildFreemiusQueryFromOptions)(checkoutOptions);
|
|
733
|
-
const url = new URL(
|
|
877
|
+
const url = new URL(`${this.getBaseUrl()}/product/${this.productId}/`);
|
|
734
878
|
url.search = queryParams;
|
|
735
|
-
return url.
|
|
879
|
+
return url.toString();
|
|
880
|
+
}
|
|
881
|
+
serialize() {
|
|
882
|
+
return {
|
|
883
|
+
options: this.getOptions(),
|
|
884
|
+
link: this.getLink(),
|
|
885
|
+
baseUrl: this.getBaseUrl()
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
getBaseUrl() {
|
|
889
|
+
return isTestServer() ? "http://checkout.freemius-local.com:8080" : "https://checkout.freemius.com";
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
|
|
893
|
+
//#endregion
|
|
894
|
+
//#region src/errors/ActionError.ts
|
|
895
|
+
var ActionError = class ActionError extends Error {
|
|
896
|
+
statusCode;
|
|
897
|
+
validationIssues;
|
|
898
|
+
constructor(message, statusCode = 400, validationIssues) {
|
|
899
|
+
super(message);
|
|
900
|
+
this.name = "ActionError";
|
|
901
|
+
this.statusCode = statusCode;
|
|
902
|
+
this.validationIssues = validationIssues;
|
|
903
|
+
}
|
|
904
|
+
toResponse() {
|
|
905
|
+
const errorResponse = { message: this.message };
|
|
906
|
+
if (this.validationIssues) errorResponse.issues = this.validationIssues;
|
|
907
|
+
return Response.json(errorResponse, { status: this.statusCode });
|
|
908
|
+
}
|
|
909
|
+
static badRequest(message) {
|
|
910
|
+
return new ActionError(message, 400);
|
|
911
|
+
}
|
|
912
|
+
static unauthorized(message = "Unauthorized") {
|
|
913
|
+
return new ActionError(message, 401);
|
|
914
|
+
}
|
|
915
|
+
static notFound(message = "Not found") {
|
|
916
|
+
return new ActionError(message, 404);
|
|
917
|
+
}
|
|
918
|
+
static validationFailed(message, validationIssues) {
|
|
919
|
+
return new ActionError(message, 400, validationIssues);
|
|
920
|
+
}
|
|
921
|
+
static internalError(message = "Internal server error") {
|
|
922
|
+
return new ActionError(message, 500);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/checkout/PricingRetriever.ts
|
|
928
|
+
var PricingRetriever = class {
|
|
929
|
+
constructor(pricing) {
|
|
930
|
+
this.pricing = pricing;
|
|
931
|
+
}
|
|
932
|
+
canHandle(request) {
|
|
933
|
+
const url = new URL(request.url);
|
|
934
|
+
const action = url.searchParams.get("action");
|
|
935
|
+
return action === "pricing_data";
|
|
936
|
+
}
|
|
937
|
+
async processAction(request) {
|
|
938
|
+
const url = new URL(request.url);
|
|
939
|
+
const topupPlanId = url.searchParams.get("topupPlanId") || void 0;
|
|
940
|
+
const pricingData = await this.pricing.retrieve(topupPlanId);
|
|
941
|
+
return Response.json(pricingData);
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/checkout/PurchaseProcessor.ts
|
|
947
|
+
var PurchaseProcessor = class {
|
|
948
|
+
constructor(purchase, callback) {
|
|
949
|
+
this.purchase = purchase;
|
|
950
|
+
this.callback = callback;
|
|
951
|
+
}
|
|
952
|
+
canHandle(request) {
|
|
953
|
+
const url = new URL(request.url);
|
|
954
|
+
const action = url.searchParams.get("action");
|
|
955
|
+
return request.method === "POST" && action === "process_purchase";
|
|
956
|
+
}
|
|
957
|
+
async processAction(request) {
|
|
958
|
+
const purchaseSchema = zod.object({ purchase: zod.object({ license_id: zod.string() }) });
|
|
959
|
+
const contentType = request.headers.get("content-type");
|
|
960
|
+
if (!contentType || !contentType.includes("application/json")) throw ActionError.badRequest("Invalid content type. Expected application/json");
|
|
961
|
+
let requestBody;
|
|
962
|
+
try {
|
|
963
|
+
requestBody = await request.json();
|
|
964
|
+
} catch {
|
|
965
|
+
throw ActionError.badRequest("Request body must be valid JSON");
|
|
966
|
+
}
|
|
967
|
+
const parseResult = purchaseSchema.safeParse(requestBody);
|
|
968
|
+
if (!parseResult.success) throw ActionError.validationFailed("Invalid request body format", parseResult.error.issues);
|
|
969
|
+
const { purchase: { license_id: licenseId } } = parseResult.data;
|
|
970
|
+
const purchase = await this.purchase.retrievePurchase(licenseId);
|
|
971
|
+
if (!purchase) throw ActionError.notFound("No purchase data found for the provided license ID");
|
|
972
|
+
if (this.callback) {
|
|
973
|
+
const callbackResponse = await this.callback(purchase);
|
|
974
|
+
if (callbackResponse) return callbackResponse;
|
|
975
|
+
}
|
|
976
|
+
return Response.json(purchase.toData());
|
|
736
977
|
}
|
|
737
978
|
};
|
|
738
979
|
|
|
@@ -766,10 +1007,10 @@ var CheckoutRedirectInfo = class {
|
|
|
766
1007
|
this.action = data.action ? data.action : null;
|
|
767
1008
|
this.amount = parseNumber(data.amount);
|
|
768
1009
|
this.tax = parseNumber(data.tax);
|
|
769
|
-
this.type = data.type === "subscription" ? "subscription" : "one-off";
|
|
770
1010
|
this.subscription_id = data.subscription_id ? idToString(data.subscription_id) : null;
|
|
771
1011
|
this.billing_cycle = data.billing_cycle ? parseBillingCycle(data.billing_cycle) : null;
|
|
772
1012
|
this.payment_id = data.payment_id ? idToString(data.payment_id) : null;
|
|
1013
|
+
this.type = this.subscription_id ? "subscription" : "one-off";
|
|
773
1014
|
}
|
|
774
1015
|
isSubscription() {
|
|
775
1016
|
return this.type === "subscription";
|
|
@@ -795,13 +1036,144 @@ var CheckoutRedirectInfo = class {
|
|
|
795
1036
|
}
|
|
796
1037
|
};
|
|
797
1038
|
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/checkout/RedirectProcessor.ts
|
|
1041
|
+
var RedirectProcessor = class {
|
|
1042
|
+
constructor(secretKey, proxyUrl, callback, afterProcessUrl) {
|
|
1043
|
+
this.secretKey = secretKey;
|
|
1044
|
+
this.proxyUrl = proxyUrl;
|
|
1045
|
+
this.callback = callback;
|
|
1046
|
+
this.afterProcessUrl = afterProcessUrl;
|
|
1047
|
+
}
|
|
1048
|
+
canHandle(request) {
|
|
1049
|
+
const url = new URL(request.url);
|
|
1050
|
+
return request.method === "GET" && url.searchParams.has("signature");
|
|
1051
|
+
}
|
|
1052
|
+
async processAction(request) {
|
|
1053
|
+
const info = await this.getRedirectInfo(request.url);
|
|
1054
|
+
if (!info) throw ActionError.badRequest("Invalid or missing redirect signature");
|
|
1055
|
+
if (this.callback) {
|
|
1056
|
+
const callbackResponse = await this.callback(info);
|
|
1057
|
+
if (callbackResponse) return callbackResponse;
|
|
1058
|
+
}
|
|
1059
|
+
const url = new URL(this.afterProcessUrl ?? this.proxyUrl ?? request.url);
|
|
1060
|
+
url.search = "";
|
|
1061
|
+
url.searchParams.set("plan", info.plan_id);
|
|
1062
|
+
url.searchParams.set("is_subscription", info.isSubscription() ? "1" : "0");
|
|
1063
|
+
url.searchParams.set("quote", info.quota?.toString() ?? "0");
|
|
1064
|
+
return Response.redirect(url.href, 302);
|
|
1065
|
+
}
|
|
1066
|
+
async getRedirectInfo(currentUrl) {
|
|
1067
|
+
const url = new URL(currentUrl.replace(/%20/g, "+"));
|
|
1068
|
+
const signature = url.searchParams.get("signature");
|
|
1069
|
+
if (!signature) return null;
|
|
1070
|
+
if (this.proxyUrl) {
|
|
1071
|
+
const proxy = new URL(this.proxyUrl);
|
|
1072
|
+
url.protocol = proxy.protocol;
|
|
1073
|
+
url.host = proxy.host;
|
|
1074
|
+
url.port = proxy.port;
|
|
1075
|
+
}
|
|
1076
|
+
const cleanUrl = this.getCleanUrl(url.href);
|
|
1077
|
+
const calculatedSignature = (0, crypto.createHmac)("sha256", this.secretKey).update(cleanUrl).digest("hex");
|
|
1078
|
+
const result = (0, crypto.timingSafeEqual)(Buffer.from(calculatedSignature), Buffer.from(signature));
|
|
1079
|
+
if (!result) return null;
|
|
1080
|
+
const params = Object.fromEntries(url.searchParams.entries());
|
|
1081
|
+
if (!params.user_id || !params.plan_id || !params.pricing_id || !params.email) return null;
|
|
1082
|
+
return new CheckoutRedirectInfo(params);
|
|
1083
|
+
}
|
|
1084
|
+
getCleanUrl(url) {
|
|
1085
|
+
const signatureParam = "&signature=";
|
|
1086
|
+
const signatureParamFirst = "?signature=";
|
|
1087
|
+
let signaturePos = url.indexOf(signatureParam);
|
|
1088
|
+
if (signaturePos === -1) signaturePos = url.indexOf(signatureParamFirst);
|
|
1089
|
+
if (signaturePos === -1) return url;
|
|
1090
|
+
return url.substring(0, signaturePos);
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
//#endregion
|
|
1095
|
+
//#region src/checkout/CheckoutRequestProcessor.ts
|
|
1096
|
+
var CheckoutRequestProcessor = class {
|
|
1097
|
+
constructor(purchase, pricing, secretKey) {
|
|
1098
|
+
this.purchase = purchase;
|
|
1099
|
+
this.pricing = pricing;
|
|
1100
|
+
this.secretKey = secretKey;
|
|
1101
|
+
}
|
|
1102
|
+
createProcessor(config) {
|
|
1103
|
+
return (request) => this.process(config, request);
|
|
1104
|
+
}
|
|
1105
|
+
async process(config, request) {
|
|
1106
|
+
const url = new URL(request.url);
|
|
1107
|
+
const action = url.searchParams.get("action");
|
|
1108
|
+
if (!action) return Response.json({ error: "Action parameter is required" }, { status: 400 });
|
|
1109
|
+
const actionHandlers = [
|
|
1110
|
+
this.getPricingRetriever(),
|
|
1111
|
+
this.getRedirectProcessor({
|
|
1112
|
+
proxyUrl: config.proxyUrl,
|
|
1113
|
+
callback: config.onRedirect,
|
|
1114
|
+
afterProcessUrl: config.afterProcessUrl
|
|
1115
|
+
}),
|
|
1116
|
+
this.getPurchaseProcessor({ callback: config.onPurchase })
|
|
1117
|
+
];
|
|
1118
|
+
try {
|
|
1119
|
+
for (const actionHandler of actionHandlers) if (actionHandler.canHandle(request)) return await actionHandler.processAction(request);
|
|
1120
|
+
} catch (error) {
|
|
1121
|
+
if (error instanceof ActionError) return error.toResponse();
|
|
1122
|
+
console.error("Error processing action:", error);
|
|
1123
|
+
return ActionError.internalError("Internal Server Error").toResponse();
|
|
1124
|
+
}
|
|
1125
|
+
return ActionError.badRequest("Unsupported action").toResponse();
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Processes the redirect from Freemius Checkout.
|
|
1129
|
+
*
|
|
1130
|
+
* This method verifies the signature in the URL and returns a CheckoutRedirectInfo object if successful.
|
|
1131
|
+
*
|
|
1132
|
+
* For nextjs like applications, make sure to replace the URL from the `Request` object with the right hostname to take care of the proxy.
|
|
1133
|
+
*
|
|
1134
|
+
* For example, if you have put the nextjs application behind nginx proxy (or ngrok during local development), then nextjs will still see the `request.url` as `https://localhost:3000/...`.
|
|
1135
|
+
* In this case, you should replace it with the actual URL of your application, like `https://xyz.ngrok-free.app/...`.
|
|
1136
|
+
*
|
|
1137
|
+
* @example
|
|
1138
|
+
* ```ts
|
|
1139
|
+
* export async function GET(request: Request) {
|
|
1140
|
+
* // Replace the URL with the actual hostname of your application
|
|
1141
|
+
* // This is important for the signature verification to work correctly.
|
|
1142
|
+
* const data = await freemius.checkout.action.getRedirectProcessor({
|
|
1143
|
+
* proxyUrl: 'https://xyz.ngrok-free.app',
|
|
1144
|
+
* async callback(info) {
|
|
1145
|
+
* // Handle the redirect info here, like creating a license, etc.
|
|
1146
|
+
* // Return a Response object to override the default redirect behavior.
|
|
1147
|
+
* return Response.redirect('/custom-success-page', 302);
|
|
1148
|
+
* },
|
|
1149
|
+
* });
|
|
1150
|
+
*
|
|
1151
|
+
* return data.processAction(request);
|
|
1152
|
+
* }
|
|
1153
|
+
* ```
|
|
1154
|
+
*/
|
|
1155
|
+
getRedirectProcessor(config) {
|
|
1156
|
+
return new RedirectProcessor(this.secretKey, config.proxyUrl, config.callback, config.afterProcessUrl);
|
|
1157
|
+
}
|
|
1158
|
+
getPurchaseProcessor(config) {
|
|
1159
|
+
return new PurchaseProcessor(this.purchase, config.callback);
|
|
1160
|
+
}
|
|
1161
|
+
getPricingRetriever() {
|
|
1162
|
+
return new PricingRetriever(this.pricing);
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
|
|
798
1166
|
//#endregion
|
|
799
1167
|
//#region src/services/CheckoutService.ts
|
|
800
1168
|
var CheckoutService = class {
|
|
801
|
-
|
|
1169
|
+
request;
|
|
1170
|
+
constructor(productId, publicKey, secretKey, purchase, pricing) {
|
|
802
1171
|
this.productId = productId;
|
|
803
1172
|
this.publicKey = publicKey;
|
|
804
1173
|
this.secretKey = secretKey;
|
|
1174
|
+
this.purchase = purchase;
|
|
1175
|
+
this.pricing = pricing;
|
|
1176
|
+
this.request = new CheckoutRequestProcessor(this.purchase, this.pricing, this.secretKey);
|
|
805
1177
|
}
|
|
806
1178
|
/**
|
|
807
1179
|
* Use this to build a Checkout for your product.
|
|
@@ -814,74 +1186,39 @@ var CheckoutService = class {
|
|
|
814
1186
|
* @example
|
|
815
1187
|
* Basic usage:
|
|
816
1188
|
* ```typescript
|
|
817
|
-
* const
|
|
818
|
-
* .
|
|
819
|
-
* .inSandbox()
|
|
820
|
-
* .withRecommendation()
|
|
821
|
-
* .toOptions(); // Or .toLink() for a hosted checkout link
|
|
1189
|
+
* const checkout = await freemius.checkout.create({user: session?.user})
|
|
1190
|
+
* .getOptions(); // Or .getLink() for a hosted checkout link
|
|
822
1191
|
* ```
|
|
823
1192
|
*
|
|
824
1193
|
* @example
|
|
825
|
-
* Advanced configuration:
|
|
1194
|
+
* Advanced configuration: You can also skip the convenience options and rather use the builder directly to configure the checkout.
|
|
1195
|
+
*
|
|
826
1196
|
* ```typescript
|
|
827
|
-
* const
|
|
828
|
-
* .
|
|
829
|
-
* .
|
|
830
|
-
* .
|
|
831
|
-
* .withCurrency('eur')
|
|
832
|
-
* .withCoupon({
|
|
1197
|
+
* const checkout = freemius.checkout.create()
|
|
1198
|
+
* .setUser(user, true)
|
|
1199
|
+
* .setPlan('1234')
|
|
1200
|
+
* .setCoupon({
|
|
833
1201
|
* code: 'DISCOUNT2023',
|
|
834
1202
|
* hideUI: false
|
|
835
1203
|
* })
|
|
836
|
-
* .
|
|
837
|
-
* .
|
|
838
|
-
* layout: 'horizontal',
|
|
839
|
-
* formPosition: 'left',
|
|
840
|
-
* fullscreen: true,
|
|
841
|
-
* modalTitle: 'Upgrade Now'
|
|
842
|
-
* })
|
|
843
|
-
* .withDiscounts({
|
|
844
|
-
* annual: true,
|
|
845
|
-
* multisite: 'auto',
|
|
846
|
-
* bundle: 'maximize',
|
|
847
|
-
* showMonthlySwitch: true
|
|
848
|
-
* })
|
|
849
|
-
* .withReviewsAndBadges({
|
|
850
|
-
* showReviews: true,
|
|
851
|
-
* showRefundBadge: true,
|
|
852
|
-
* refundPolicyPosition: 'below_form'
|
|
853
|
-
* })
|
|
854
|
-
* .withBillingCycle('dropdown', 'annual')
|
|
855
|
-
* .withLocale('en_US')
|
|
856
|
-
* .withAffiliate(12345)
|
|
857
|
-
* .inSandbox()
|
|
858
|
-
* .toOptions();
|
|
1204
|
+
* .setSandbox()
|
|
1205
|
+
* .getOptions();
|
|
859
1206
|
* ```
|
|
860
|
-
*/
|
|
861
|
-
create(withRecommendation = true) {
|
|
862
|
-
const productId = idToString(this.productId);
|
|
863
|
-
const builder = new CheckoutBuilder({}, productId, this.publicKey, this.secretKey);
|
|
864
|
-
return withRecommendation ? builder.withRecommendation() : builder;
|
|
865
|
-
}
|
|
866
|
-
/**
|
|
867
|
-
* Convenience method to create checkout options for a specific user with or without sandbox mode.
|
|
868
|
-
*
|
|
869
|
-
* Useful for generating recommended checkout options for SaaS.
|
|
870
|
-
*/
|
|
871
|
-
createUserOptions(user, isSandbox = false) {
|
|
872
|
-
let builder = this.create().withUser(user);
|
|
873
|
-
if (isSandbox) builder = builder.inSandbox();
|
|
874
|
-
return builder.toOptions();
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* Convenience method to create a checkout link for a specific user with or without sandbox mode.
|
|
878
1207
|
*
|
|
879
|
-
*
|
|
1208
|
+
* @example
|
|
880
1209
|
*/
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1210
|
+
async create(options = {}) {
|
|
1211
|
+
const { user, isSandbox = false, withRecommendation = true, title, image, planId, quota, trial } = options;
|
|
1212
|
+
const builder = new Checkout(idToString(this.productId), this.publicKey, this.secretKey);
|
|
1213
|
+
if (user) builder.setUser(user, true);
|
|
1214
|
+
if (withRecommendation) builder.setRecommendations();
|
|
1215
|
+
if (isSandbox) builder.setSandbox();
|
|
1216
|
+
if (title) builder.setTitle(title);
|
|
1217
|
+
if (image) builder.setImage(image);
|
|
1218
|
+
if (planId) builder.setPlan(planId);
|
|
1219
|
+
if (quota) builder.setQuota(quota);
|
|
1220
|
+
if (trial) builder.setTrial(trial);
|
|
1221
|
+
return builder;
|
|
885
1222
|
}
|
|
886
1223
|
/**
|
|
887
1224
|
* Retrieves the sandbox parameters for the checkout.
|
|
@@ -894,114 +1231,159 @@ var CheckoutService = class {
|
|
|
894
1231
|
* Also think about whether we should make the builder's `inSandbox` method async as well.
|
|
895
1232
|
*/
|
|
896
1233
|
async getSandboxParams() {
|
|
897
|
-
|
|
898
|
-
const token = `${timestamp}${this.productId}${this.secretKey}${this.publicKey}checkout`;
|
|
899
|
-
return {
|
|
900
|
-
ctx: timestamp,
|
|
901
|
-
token: (0, crypto.createHash)("md5").update(token).digest("hex")
|
|
902
|
-
};
|
|
1234
|
+
return Checkout.createSandboxToken(idToString(this.productId), this.secretKey, this.publicKey);
|
|
903
1235
|
}
|
|
904
1236
|
/**
|
|
905
|
-
* Processes the redirect
|
|
1237
|
+
* Processes a redirect URL and returns the checkout redirect information if valid.
|
|
906
1238
|
*
|
|
907
|
-
* This
|
|
1239
|
+
* This is useful for handling redirects from the checkout portal back to your application.
|
|
908
1240
|
*
|
|
909
|
-
*
|
|
1241
|
+
* @param url The current URL to process.
|
|
1242
|
+
* @param proxyUrl Optional proxy URL to replace parts of the URL for signature verification.
|
|
910
1243
|
*
|
|
911
|
-
*
|
|
912
|
-
* In this case, you should replace it with the actual URL of your application, like `https://xyz.ngrok-free.app/...`.
|
|
1244
|
+
* @returns A promise that resolves to the checkout redirect information or null if invalid.
|
|
913
1245
|
*
|
|
914
1246
|
* @example
|
|
915
|
-
* ```
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
1247
|
+
* ```typescript
|
|
1248
|
+
* const redirectInfo = await freemius.checkout.processRedirect(window.location.href);
|
|
1249
|
+
*
|
|
1250
|
+
* if (redirectInfo) {
|
|
1251
|
+
* // Handle valid redirect info
|
|
1252
|
+
* } else {
|
|
1253
|
+
* // Handle invalid or missing redirect info
|
|
922
1254
|
* }
|
|
923
1255
|
* ```
|
|
924
1256
|
*/
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
if (!signature) return null;
|
|
929
|
-
const cleanUrl = this.getCleanUrl(url.href);
|
|
930
|
-
const calculatedSignature = (0, crypto.createHmac)("sha256", this.secretKey).update(cleanUrl).digest("hex");
|
|
931
|
-
const result = (0, crypto.timingSafeEqual)(Buffer.from(calculatedSignature), Buffer.from(signature));
|
|
932
|
-
if (!result) return null;
|
|
933
|
-
const params = Object.fromEntries(url.searchParams.entries());
|
|
934
|
-
if (!params.user_id || !params.plan_id || !params.pricing_id || !params.email) return null;
|
|
935
|
-
return new CheckoutRedirectInfo(params);
|
|
936
|
-
}
|
|
937
|
-
getCleanUrl(url) {
|
|
938
|
-
const signatureParam = "&signature=";
|
|
939
|
-
const signatureParamFirst = "?signature=";
|
|
940
|
-
let signaturePos = url.indexOf(signatureParam);
|
|
941
|
-
if (signaturePos === -1) signaturePos = url.indexOf(signatureParamFirst);
|
|
942
|
-
if (signaturePos === -1) return url;
|
|
943
|
-
return url.substring(0, signaturePos);
|
|
1257
|
+
processRedirect(url, proxyUrl) {
|
|
1258
|
+
const processor = new RedirectProcessor(this.secretKey, proxyUrl);
|
|
1259
|
+
return processor.getRedirectInfo(url);
|
|
944
1260
|
}
|
|
945
1261
|
};
|
|
946
1262
|
|
|
947
1263
|
//#endregion
|
|
948
|
-
//#region src/
|
|
949
|
-
var
|
|
950
|
-
constructor(api, checkout) {
|
|
1264
|
+
//#region src/customer-portal/PortalDataRepository.ts
|
|
1265
|
+
var PortalDataRepository = class {
|
|
1266
|
+
constructor(api, action, checkout) {
|
|
951
1267
|
this.api = api;
|
|
1268
|
+
this.action = action;
|
|
952
1269
|
this.checkout = checkout;
|
|
953
1270
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1271
|
+
async retrievePortalDataByEmail(config) {
|
|
1272
|
+
const user = await this.api.user.retrieveByEmail(config.email);
|
|
1273
|
+
if (!user) return null;
|
|
1274
|
+
return this.retrievePortalData({
|
|
1275
|
+
user,
|
|
1276
|
+
endpoint: config.endpoint,
|
|
1277
|
+
primaryLicenseId: config.primaryLicenseId ?? null,
|
|
1278
|
+
sandbox: config.sandbox ?? false
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
async retrievePortalDataByUserId(config) {
|
|
1282
|
+
const user = await this.api.user.retrieve(config.userId);
|
|
1283
|
+
if (!user) return null;
|
|
1284
|
+
return this.retrievePortalData({
|
|
1285
|
+
user,
|
|
1286
|
+
endpoint: config.endpoint,
|
|
1287
|
+
primaryLicenseId: config.primaryLicenseId ?? null,
|
|
1288
|
+
sandbox: config.sandbox ?? false
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
async retrievePortalData(config) {
|
|
1292
|
+
const { user, endpoint, primaryLicenseId = null, sandbox = false } = config;
|
|
1293
|
+
const userId = user.id;
|
|
1294
|
+
const data = await this.retrieveApiData(userId);
|
|
1295
|
+
if (!data) return null;
|
|
1296
|
+
const { pricingData, subscriptions, payments, billing, coupons } = data;
|
|
1297
|
+
const plans = this.getPlansById(pricingData);
|
|
1298
|
+
const pricings = this.getPricingById(pricingData);
|
|
979
1299
|
const checkoutOptions = { product_id: this.api.productId };
|
|
980
1300
|
if (sandbox) checkoutOptions.sandbox = await this.checkout.getSandboxParams();
|
|
981
|
-
const
|
|
1301
|
+
const portalData = {
|
|
1302
|
+
endpoint,
|
|
982
1303
|
user,
|
|
983
1304
|
checkoutOptions,
|
|
984
|
-
billing,
|
|
985
|
-
subscriptions:
|
|
986
|
-
|
|
987
|
-
active: [],
|
|
988
|
-
past: []
|
|
989
|
-
},
|
|
990
|
-
payments: portalPayments,
|
|
1305
|
+
billing: this.getBilling(billing, userId, endpoint),
|
|
1306
|
+
subscriptions: await this.getSubscriptions(subscriptions, plans, pricings, primaryLicenseId, endpoint),
|
|
1307
|
+
payments: this.getPayments(payments, plans, pricings, userId, endpoint),
|
|
991
1308
|
plans: pricingData.plans ?? [],
|
|
992
|
-
sellingUnit: pricingData.selling_unit_label ?? {
|
|
1309
|
+
sellingUnit: pricingData.plugin?.selling_unit_label ?? {
|
|
993
1310
|
singular: "Unit",
|
|
994
1311
|
plural: "Units"
|
|
995
1312
|
},
|
|
996
|
-
productId: this.api.productId
|
|
1313
|
+
productId: this.api.productId,
|
|
1314
|
+
cancellationCoupons: coupons
|
|
1315
|
+
};
|
|
1316
|
+
return portalData;
|
|
1317
|
+
}
|
|
1318
|
+
async retrieveApiData(userId) {
|
|
1319
|
+
const [pricingData, subscriptions, payments, billing, coupons] = await Promise.all([
|
|
1320
|
+
this.api.product.retrievePricingData(),
|
|
1321
|
+
this.api.user.retrieveSubscriptions(userId, {
|
|
1322
|
+
extended: true,
|
|
1323
|
+
enrich_with_cancellation_discounts: true
|
|
1324
|
+
}),
|
|
1325
|
+
this.api.user.retrievePayments(userId),
|
|
1326
|
+
this.api.user.retrieveBilling(userId),
|
|
1327
|
+
this.api.product.retrieveSubscriptionCancellationCoupon()
|
|
1328
|
+
]);
|
|
1329
|
+
if (!pricingData || !subscriptions) return null;
|
|
1330
|
+
return {
|
|
1331
|
+
pricingData,
|
|
1332
|
+
subscriptions,
|
|
1333
|
+
payments,
|
|
1334
|
+
billing,
|
|
1335
|
+
coupons
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
getPayments(payments, plans, pricings, userId, endpoint) {
|
|
1339
|
+
return payments.map((payment) => ({
|
|
1340
|
+
...payment,
|
|
1341
|
+
invoiceUrl: this.action.invoice.createAuthenticatedUrl(payment.id, idToString(userId), endpoint),
|
|
1342
|
+
paymentMethod: parsePaymentMethod(payment.gateway),
|
|
1343
|
+
createdAt: parseDateTime(payment.created) ?? /* @__PURE__ */ new Date(),
|
|
1344
|
+
planTitle: plans.get(payment.plan_id)?.title ?? `Plan ${payment.plan_id}`,
|
|
1345
|
+
quota: pricings.get(payment.pricing_id)?.licenses ?? null
|
|
1346
|
+
}));
|
|
1347
|
+
}
|
|
1348
|
+
getPlansById(pricingData) {
|
|
1349
|
+
const plans = /* @__PURE__ */ new Map();
|
|
1350
|
+
pricingData.plans?.forEach((plan) => {
|
|
1351
|
+
plan.title = plan.title ?? plan.name ?? `Plan ${plan.id}`;
|
|
1352
|
+
plans.set(idToString(plan.id), plan);
|
|
1353
|
+
});
|
|
1354
|
+
return plans;
|
|
1355
|
+
}
|
|
1356
|
+
getPricingById(pricingData) {
|
|
1357
|
+
const pricings = /* @__PURE__ */ new Map();
|
|
1358
|
+
pricingData.plans?.forEach((plan) => {
|
|
1359
|
+
plan.pricing?.forEach((p) => {
|
|
1360
|
+
pricings.set(idToString(p.id), p);
|
|
1361
|
+
});
|
|
1362
|
+
});
|
|
1363
|
+
return pricings;
|
|
1364
|
+
}
|
|
1365
|
+
getBilling(billing, userId, endpoint) {
|
|
1366
|
+
return {
|
|
1367
|
+
...billing ?? {},
|
|
1368
|
+
updateUrl: this.action.billing.createAuthenticatedUrl(billing?.id ?? "new", idToString(userId), endpoint)
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
async getSubscriptions(subscriptions, plans, pricings, primaryLicenseId = null, endpoint) {
|
|
1372
|
+
const portalSubscriptions = {
|
|
1373
|
+
primary: null,
|
|
1374
|
+
active: [],
|
|
1375
|
+
past: []
|
|
997
1376
|
};
|
|
998
1377
|
subscriptions.forEach((subscription) => {
|
|
999
1378
|
const isActive = null === subscription.canceled_at;
|
|
1379
|
+
const trialEndsData = subscription.trial_ends ? parseDateTime(subscription.trial_ends) : null;
|
|
1380
|
+
const isTrial = trialEndsData ? trialEndsData > /* @__PURE__ */ new Date() : false;
|
|
1381
|
+
const isFreeTrial = isTrial && !subscription.gateway;
|
|
1000
1382
|
const subscriptionData = {
|
|
1001
1383
|
subscriptionId: idToString(subscription.id),
|
|
1002
1384
|
planId: idToString(subscription.plan_id),
|
|
1003
1385
|
pricingId: idToString(subscription.pricing_id),
|
|
1004
|
-
planTitle:
|
|
1386
|
+
planTitle: plans.get(subscription.plan_id)?.title ?? `Plan ${subscription.plan_id}`,
|
|
1005
1387
|
renewalAmount: parseNumber(subscription.renewal_amount),
|
|
1006
1388
|
initialAmount: parseNumber(subscription.initial_amount),
|
|
1007
1389
|
billingCycle: parseBillingCycle(subscription.billing_cycle),
|
|
@@ -1011,207 +1393,470 @@ var CustomerPortalService = class {
|
|
|
1011
1393
|
currency: parseCurrency(subscription.currency) ?? CURRENCY.USD,
|
|
1012
1394
|
createdAt: parseDateTime(subscription.created) ?? /* @__PURE__ */ new Date(),
|
|
1013
1395
|
cancelledAt: subscription.canceled_at ? parseDateTime(subscription.canceled_at) : null,
|
|
1014
|
-
quota:
|
|
1015
|
-
paymentMethod: parsePaymentMethod(subscription.gateway)
|
|
1396
|
+
quota: pricings.get(subscription.pricing_id)?.licenses ?? null,
|
|
1397
|
+
paymentMethod: parsePaymentMethod(subscription.gateway),
|
|
1398
|
+
isTrial,
|
|
1399
|
+
trialEnds: isTrial ? trialEndsData : null,
|
|
1400
|
+
isFreeTrial,
|
|
1401
|
+
applyRenewalCancellationCouponUrl: this.isRenewalCancellationCouponApplicable(subscription) ? this.action.renewalCoupon.createAuthenticatedUrl(idToString(subscription.id), idToString(subscription.user_id), endpoint) : null,
|
|
1402
|
+
cancelRenewalUrl: this.action.cancelRenewal.createAuthenticatedUrl(idToString(subscription.id), idToString(subscription.user_id), endpoint)
|
|
1016
1403
|
};
|
|
1017
|
-
if (isActive)
|
|
1018
|
-
else
|
|
1019
|
-
if (isActive && primaryLicenseId && isIdsEqual(subscription.license_id, primaryLicenseId))
|
|
1404
|
+
if (isActive) portalSubscriptions.active.push(subscriptionData);
|
|
1405
|
+
else portalSubscriptions.past.push(subscriptionData);
|
|
1406
|
+
if (isActive && primaryLicenseId && isIdsEqual(subscription.license_id, primaryLicenseId)) portalSubscriptions.primary = subscriptionData;
|
|
1020
1407
|
});
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
if (!
|
|
1024
|
-
if (
|
|
1025
|
-
return
|
|
1408
|
+
portalSubscriptions.active.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1409
|
+
portalSubscriptions.past.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
|
1410
|
+
if (!portalSubscriptions.primary) portalSubscriptions.primary = portalSubscriptions.active[0] ?? portalSubscriptions.past[0] ?? null;
|
|
1411
|
+
if (portalSubscriptions.primary) portalSubscriptions.primary.checkoutUpgradeAuthorization = await this.api.license.retrieveCheckoutUpgradeAuthorization(portalSubscriptions.primary.licenseId);
|
|
1412
|
+
return portalSubscriptions;
|
|
1026
1413
|
}
|
|
1027
1414
|
/**
|
|
1028
|
-
*
|
|
1415
|
+
* Check if coupon application is impossible due to certain conditions.
|
|
1416
|
+
* This function can be used to determine if a coupon can be applied to a subscription.
|
|
1417
|
+
* Introduced initially for PayPal subscriptions with a renewal date less than 48 hours in the future.
|
|
1418
|
+
*
|
|
1419
|
+
* @author @DanieleAlessandra
|
|
1420
|
+
* @author @swashata (Ported to SDK)
|
|
1421
|
+
*
|
|
1422
|
+
* @returns boolean
|
|
1029
1423
|
*/
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1424
|
+
isRenewalCancellationCouponApplicable(subscription) {
|
|
1425
|
+
if (subscription.has_subscription_cancellation_discount || (subscription.total_gross ?? 0) <= 0) return false;
|
|
1426
|
+
if (subscription.gateway !== "paypal") return true;
|
|
1427
|
+
const nextPaymentTime = parseDateTime(subscription.next_payment)?.getTime() ?? 0;
|
|
1428
|
+
const currentTime = (/* @__PURE__ */ new Date()).getTime();
|
|
1429
|
+
const fortyEightHoursInMs = 2880 * 60 * 1e3;
|
|
1430
|
+
return nextPaymentTime <= currentTime + fortyEightHoursInMs;
|
|
1036
1431
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
//#endregion
|
|
1435
|
+
//#region src/customer-portal/PortalDataRetriever.ts
|
|
1436
|
+
var PortalDataRetriever = class {
|
|
1437
|
+
constructor(repository, getUser, endpoint, isSandbox) {
|
|
1438
|
+
this.repository = repository;
|
|
1439
|
+
this.getUser = getUser;
|
|
1440
|
+
this.endpoint = endpoint;
|
|
1441
|
+
this.isSandbox = isSandbox;
|
|
1442
|
+
}
|
|
1443
|
+
createAuthenticatedUrl() {
|
|
1444
|
+
throw new Error("Method not implemented.");
|
|
1445
|
+
}
|
|
1446
|
+
verifyAuthentication() {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
canHandle(request) {
|
|
1450
|
+
const url = new URL(request.url);
|
|
1451
|
+
const action = url.searchParams.get("action");
|
|
1452
|
+
return request.method === "GET" && action === "portal_data";
|
|
1453
|
+
}
|
|
1454
|
+
async processAction() {
|
|
1455
|
+
const user = await this.getUser();
|
|
1456
|
+
if (!user || !("id" in user)) return Response.json(null);
|
|
1457
|
+
return Response.json(await this.repository.retrievePortalDataByUserId({
|
|
1458
|
+
userId: user.id,
|
|
1459
|
+
endpoint: this.endpoint,
|
|
1460
|
+
primaryLicenseId: null,
|
|
1461
|
+
sandbox: this.isSandbox ?? false
|
|
1462
|
+
}));
|
|
1045
1463
|
}
|
|
1046
1464
|
};
|
|
1047
1465
|
|
|
1048
1466
|
//#endregion
|
|
1049
|
-
//#region src/
|
|
1050
|
-
var
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
this.
|
|
1073
|
-
this.
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
this.subscriptionId = null;
|
|
1079
|
-
this.billingCycle = null;
|
|
1080
|
-
this.quota = license.quota ?? null;
|
|
1081
|
-
this.pricingId = idToString(license.pricing_id);
|
|
1082
|
-
this.initialAmount = null;
|
|
1083
|
-
this.renewalAmount = null;
|
|
1084
|
-
this.currency = null;
|
|
1085
|
-
this.renewalDate = null;
|
|
1086
|
-
this.paymentMethod = null;
|
|
1087
|
-
this.created = parseDateTime(license.created) ?? /* @__PURE__ */ new Date();
|
|
1088
|
-
if (subscription) {
|
|
1089
|
-
this.subscriptionId = idToString(subscription.id);
|
|
1090
|
-
this.billingCycle = parseBillingCycle(subscription.billing_cycle);
|
|
1091
|
-
this.initialAmount = parseNumber(subscription.initial_amount);
|
|
1092
|
-
this.renewalAmount = parseNumber(subscription.renewal_amount);
|
|
1093
|
-
this.currency = parseCurrency(subscription.currency);
|
|
1094
|
-
this.renewalDate = subscription.next_payment ? parseDateTime(subscription.next_payment) : null;
|
|
1095
|
-
this.paymentMethod = parsePaymentMethod(subscription.gateway);
|
|
1467
|
+
//#region src/customer-portal/PurchaseRestorer.ts
|
|
1468
|
+
var PurchaseRestorer = class {
|
|
1469
|
+
constructor(purchase, user, callback, subscriptionOnly = false) {
|
|
1470
|
+
this.purchase = purchase;
|
|
1471
|
+
this.user = user;
|
|
1472
|
+
this.callback = callback;
|
|
1473
|
+
this.subscriptionOnly = subscriptionOnly;
|
|
1474
|
+
}
|
|
1475
|
+
createAuthenticatedUrl() {
|
|
1476
|
+
throw new Error("Method not implemented.");
|
|
1477
|
+
}
|
|
1478
|
+
verifyAuthentication() {
|
|
1479
|
+
return true;
|
|
1480
|
+
}
|
|
1481
|
+
canHandle(request) {
|
|
1482
|
+
const url = new URL(request.url);
|
|
1483
|
+
const action = url.searchParams.get("action");
|
|
1484
|
+
return request.method === "POST" && action === "restore_purchase";
|
|
1485
|
+
}
|
|
1486
|
+
async processAction() {
|
|
1487
|
+
let purchases = null;
|
|
1488
|
+
const user = await this.user();
|
|
1489
|
+
if (!user) throw ActionError.unauthorized("User not authenticated");
|
|
1490
|
+
if (this.subscriptionOnly) purchases = "id" in user ? await this.purchase.retrieveSubscriptions(user.id) : await this.purchase.retrieveSubscriptionsByEmail(user.email);
|
|
1491
|
+
else purchases = "id" in user ? await this.purchase.retrievePurchases(user.id) : await this.purchase.retrievePurchasesByEmail(user.email);
|
|
1492
|
+
if (!purchases) throw ActionError.notFound("No purchases found for the provided user");
|
|
1493
|
+
if (this.callback) {
|
|
1494
|
+
const callbackResponse = await this.callback(purchases);
|
|
1495
|
+
if (callbackResponse) return callbackResponse;
|
|
1096
1496
|
}
|
|
1497
|
+
return Response.json(purchases.map((p) => p.toData()));
|
|
1097
1498
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1499
|
+
};
|
|
1500
|
+
|
|
1501
|
+
//#endregion
|
|
1502
|
+
//#region src/customer-portal/PortalRequestProcessor.ts
|
|
1503
|
+
var PortalRequestProcessor = class {
|
|
1504
|
+
constructor(repository, action, purchase) {
|
|
1505
|
+
this.repository = repository;
|
|
1506
|
+
this.action = action;
|
|
1507
|
+
this.purchase = purchase;
|
|
1100
1508
|
}
|
|
1101
|
-
|
|
1102
|
-
return
|
|
1509
|
+
createProcessor(config) {
|
|
1510
|
+
return (request) => this.process(config, request);
|
|
1103
1511
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
renewalDate: this.renewalDate,
|
|
1123
|
-
paymentMethod: this.paymentMethod,
|
|
1124
|
-
created: this.created
|
|
1125
|
-
};
|
|
1512
|
+
/**
|
|
1513
|
+
* Process actions done by the user in the customer portal.
|
|
1514
|
+
*/
|
|
1515
|
+
async process(config, request) {
|
|
1516
|
+
const url = new URL(request.url);
|
|
1517
|
+
const action = url.searchParams.get("action");
|
|
1518
|
+
if (!action) return ActionError.badRequest("Action parameter is required").toResponse();
|
|
1519
|
+
const actionHandlers = [new PortalDataRetriever(this.repository, config.getUser, config.portalEndpoint, config.isSandbox), ...this.action.getAllHandlers()];
|
|
1520
|
+
if (config.onRestore) actionHandlers.push(new PurchaseRestorer(this.purchase, config.getUser, config.onRestore, config.restoreSubscriptionsOnly ?? false));
|
|
1521
|
+
try {
|
|
1522
|
+
for (const actionHandler of actionHandlers) if (actionHandler.canHandle(request)) if (actionHandler.verifyAuthentication(request)) return await actionHandler.processAction(request);
|
|
1523
|
+
else throw ActionError.unauthorized("Invalid authentication token");
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
if (error instanceof ActionError) return error.toResponse();
|
|
1526
|
+
console.error("Error processing action:", error);
|
|
1527
|
+
return ActionError.internalError("Internal Server Error").toResponse();
|
|
1528
|
+
}
|
|
1529
|
+
return ActionError.badRequest("Unsupported action").toResponse();
|
|
1126
1530
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1531
|
+
};
|
|
1532
|
+
|
|
1533
|
+
//#endregion
|
|
1534
|
+
//#region src/customer-portal/BillingAction.ts
|
|
1535
|
+
const schema$2 = zod.object({
|
|
1536
|
+
business_name: zod.string().optional(),
|
|
1537
|
+
tax_id: zod.string().optional(),
|
|
1538
|
+
phone: zod.string().optional(),
|
|
1539
|
+
address_apt: zod.string().optional(),
|
|
1540
|
+
address_street: zod.string().optional(),
|
|
1541
|
+
address_city: zod.string().optional(),
|
|
1542
|
+
address_state: zod.string().optional(),
|
|
1543
|
+
address_country_code: zod.string().optional(),
|
|
1544
|
+
address_zip: zod.string().optional()
|
|
1545
|
+
});
|
|
1546
|
+
var BillingAction = class {
|
|
1547
|
+
actionName = "billing";
|
|
1548
|
+
constructor(api, auth) {
|
|
1549
|
+
this.api = api;
|
|
1550
|
+
this.auth = auth;
|
|
1551
|
+
}
|
|
1552
|
+
createAction(id) {
|
|
1553
|
+
return `billing_${id}`;
|
|
1554
|
+
}
|
|
1555
|
+
createAuthenticatedUrl(id, userId, endpoint) {
|
|
1556
|
+
const token = this.auth.createActionToken(this.createAction(id), userId);
|
|
1557
|
+
const url = new URL(endpoint);
|
|
1558
|
+
url.searchParams.set("action", this.actionName);
|
|
1559
|
+
url.searchParams.set("token", token);
|
|
1560
|
+
url.searchParams.set("billingId", id);
|
|
1561
|
+
url.searchParams.set("userId", userId);
|
|
1562
|
+
return url.href;
|
|
1131
1563
|
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1564
|
+
verifyAuthentication(request) {
|
|
1565
|
+
const url = new URL(request.url);
|
|
1566
|
+
const action = url.searchParams.get("action");
|
|
1567
|
+
const token = url.searchParams.get("token");
|
|
1568
|
+
const billingId = url.searchParams.get("billingId");
|
|
1569
|
+
const userId = url.searchParams.get("userId");
|
|
1570
|
+
if (!action || !token || !billingId || !userId) return false;
|
|
1571
|
+
return this.auth.verifyActionToken(token, this.createAction(billingId), userId);
|
|
1572
|
+
}
|
|
1573
|
+
canHandle(request) {
|
|
1574
|
+
const url = new URL(request.url);
|
|
1575
|
+
const action = url.searchParams.get("action");
|
|
1576
|
+
return action === this.actionName;
|
|
1577
|
+
}
|
|
1578
|
+
async processAction(request) {
|
|
1579
|
+
const contentType = request.headers.get("content-type");
|
|
1580
|
+
if (!contentType || !contentType.includes("application/json")) throw ActionError.badRequest("Invalid content type. Expected application/json");
|
|
1581
|
+
let requestBody;
|
|
1582
|
+
try {
|
|
1583
|
+
requestBody = await request.json();
|
|
1584
|
+
} catch {
|
|
1585
|
+
throw ActionError.badRequest("Request body must be valid JSON");
|
|
1586
|
+
}
|
|
1587
|
+
const parseResult = schema$2.safeParse(requestBody);
|
|
1588
|
+
if (!parseResult.success) throw ActionError.validationFailed("Invalid request body format", parseResult.error.issues);
|
|
1589
|
+
const billingData = parseResult.data;
|
|
1590
|
+
const url = new URL(request.url);
|
|
1591
|
+
const billingId = url.searchParams.get("billingId");
|
|
1592
|
+
const userId = url.searchParams.get("userId");
|
|
1593
|
+
if (!billingId) throw ActionError.badRequest("Missing required parameter: billingId");
|
|
1594
|
+
if (!userId) throw ActionError.badRequest("Missing required parameter: userId");
|
|
1595
|
+
const payload = {};
|
|
1596
|
+
if (billingData.business_name) payload.business_name = billingData.business_name;
|
|
1597
|
+
if (billingData.tax_id) payload.tax_id = billingData.tax_id;
|
|
1598
|
+
if (billingData.phone) payload.phone = billingData.phone;
|
|
1599
|
+
if (billingData.address_apt) payload.address_apt = billingData.address_apt;
|
|
1600
|
+
if (billingData.address_street) payload.address_street = billingData.address_street;
|
|
1601
|
+
if (billingData.address_city) payload.address_city = billingData.address_city;
|
|
1602
|
+
if (billingData.address_state) payload.address_state = billingData.address_state;
|
|
1603
|
+
if (billingData.address_country_code) payload.address_country_code = billingData.address_country_code;
|
|
1604
|
+
if (billingData.address_zip) payload.address_zip = billingData.address_zip;
|
|
1605
|
+
const response = await this.api.user.updateBilling(userId, payload);
|
|
1606
|
+
if (!response) throw ActionError.internalError("Failed to update billing information");
|
|
1607
|
+
return Response.json(response, { status: 200 });
|
|
1134
1608
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1609
|
+
};
|
|
1610
|
+
|
|
1611
|
+
//#endregion
|
|
1612
|
+
//#region src/customer-portal/InvoiceAction.ts
|
|
1613
|
+
var InvoiceAction = class {
|
|
1614
|
+
actionName = "invoice";
|
|
1615
|
+
constructor(api, auth) {
|
|
1616
|
+
this.api = api;
|
|
1617
|
+
this.auth = auth;
|
|
1618
|
+
}
|
|
1619
|
+
createAction(id) {
|
|
1620
|
+
return `invoice_${id}`;
|
|
1621
|
+
}
|
|
1622
|
+
createAuthenticatedUrl(id, userId, endpoint) {
|
|
1623
|
+
const token = this.auth.createActionToken(this.createAction(id), userId);
|
|
1624
|
+
const url = new URL(endpoint);
|
|
1625
|
+
url.searchParams.set("action", this.actionName);
|
|
1626
|
+
url.searchParams.set("token", token);
|
|
1627
|
+
url.searchParams.set("invoiceId", id);
|
|
1628
|
+
url.searchParams.set("userId", userId);
|
|
1629
|
+
return url.href;
|
|
1137
1630
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1631
|
+
verifyAuthentication(request) {
|
|
1632
|
+
const url = new URL(request.url);
|
|
1633
|
+
const action = url.searchParams.get("action");
|
|
1634
|
+
const token = url.searchParams.get("token");
|
|
1635
|
+
const invoiceId = url.searchParams.get("invoiceId");
|
|
1636
|
+
const userId = url.searchParams.get("userId");
|
|
1637
|
+
if (!action || !token || !invoiceId || !userId) return false;
|
|
1638
|
+
return this.auth.verifyActionToken(token, this.createAction(invoiceId), userId);
|
|
1639
|
+
}
|
|
1640
|
+
canHandle(request) {
|
|
1641
|
+
const url = new URL(request.url);
|
|
1642
|
+
const action = url.searchParams.get("action");
|
|
1643
|
+
return action === this.actionName;
|
|
1644
|
+
}
|
|
1645
|
+
async processAction(request) {
|
|
1646
|
+
const schema$3 = zod.object({
|
|
1647
|
+
invoiceId: zod.string().min(1, "Invoice ID is required"),
|
|
1648
|
+
userId: zod.string().min(1, "User ID is required")
|
|
1649
|
+
});
|
|
1650
|
+
const url = new URL(request.url);
|
|
1651
|
+
const invoiceId = url.searchParams.get("invoiceId");
|
|
1652
|
+
const userIdParam = url.searchParams.get("userId");
|
|
1653
|
+
const parseResult = schema$3.safeParse({
|
|
1654
|
+
invoiceId,
|
|
1655
|
+
userId: userIdParam
|
|
1656
|
+
});
|
|
1657
|
+
if (!parseResult.success) throw ActionError.validationFailed("Invalid request parameters", parseResult.error.issues);
|
|
1658
|
+
const { invoiceId: validInvoiceId } = parseResult.data;
|
|
1659
|
+
try {
|
|
1660
|
+
const pdf = await this.api.payment.retrieveInvoice(validInvoiceId);
|
|
1661
|
+
if (pdf) return new Response(pdf, { headers: {
|
|
1662
|
+
"Content-Type": "application/pdf",
|
|
1663
|
+
"Content-Disposition": `inline; filename="invoice_${validInvoiceId}.pdf"`
|
|
1664
|
+
} });
|
|
1665
|
+
else throw ActionError.notFound("Invoice not found");
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
if (error instanceof ActionError) throw error;
|
|
1668
|
+
throw ActionError.internalError("Failed to retrieve invoice");
|
|
1669
|
+
}
|
|
1140
1670
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1671
|
+
};
|
|
1672
|
+
|
|
1673
|
+
//#endregion
|
|
1674
|
+
//#region src/customer-portal/SubscriptionCancellationAction.ts
|
|
1675
|
+
const schema$1 = zod.object({
|
|
1676
|
+
feedback: zod.string().optional(),
|
|
1677
|
+
reason_ids: zod.array(zod.enum([
|
|
1678
|
+
"1",
|
|
1679
|
+
"2",
|
|
1680
|
+
"3",
|
|
1681
|
+
"4",
|
|
1682
|
+
"5",
|
|
1683
|
+
"6",
|
|
1684
|
+
"7",
|
|
1685
|
+
"8",
|
|
1686
|
+
"9",
|
|
1687
|
+
"10",
|
|
1688
|
+
"11",
|
|
1689
|
+
"12",
|
|
1690
|
+
"13",
|
|
1691
|
+
"14",
|
|
1692
|
+
"15"
|
|
1693
|
+
])).optional()
|
|
1694
|
+
});
|
|
1695
|
+
var SubscriptionCancellationAction = class {
|
|
1696
|
+
actionName = "subscription_cancellation";
|
|
1697
|
+
constructor(api, auth) {
|
|
1698
|
+
this.api = api;
|
|
1699
|
+
this.auth = auth;
|
|
1700
|
+
}
|
|
1701
|
+
createAction(id) {
|
|
1702
|
+
return `cancellation_${id}`;
|
|
1703
|
+
}
|
|
1704
|
+
createAuthenticatedUrl(id, userId, endpoint) {
|
|
1705
|
+
const token = this.auth.createActionToken(this.createAction(id), userId);
|
|
1706
|
+
const url = new URL(endpoint);
|
|
1707
|
+
url.searchParams.set("action", this.actionName);
|
|
1708
|
+
url.searchParams.set("token", token);
|
|
1709
|
+
url.searchParams.set("subscriptionId", id);
|
|
1710
|
+
url.searchParams.set("userId", idToString(userId));
|
|
1711
|
+
return url.href;
|
|
1143
1712
|
}
|
|
1144
|
-
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1713
|
+
verifyAuthentication(request) {
|
|
1714
|
+
const url = new URL(request.url);
|
|
1715
|
+
const action = url.searchParams.get("action");
|
|
1716
|
+
const token = url.searchParams.get("token");
|
|
1717
|
+
const subscriptionId = url.searchParams.get("subscriptionId");
|
|
1718
|
+
const userId = url.searchParams.get("userId");
|
|
1719
|
+
if (!action || !token || !subscriptionId || !userId) return false;
|
|
1720
|
+
return this.auth.verifyActionToken(token, this.createAction(subscriptionId), userId);
|
|
1721
|
+
}
|
|
1722
|
+
canHandle(request) {
|
|
1723
|
+
const url = new URL(request.url);
|
|
1724
|
+
const action = url.searchParams.get("action");
|
|
1725
|
+
return action === this.actionName && request.method === "POST";
|
|
1726
|
+
}
|
|
1727
|
+
async processAction(request) {
|
|
1728
|
+
const contentType = request.headers.get("content-type");
|
|
1729
|
+
if (!contentType || !contentType.includes("application/json")) throw ActionError.badRequest("Invalid content type. Expected application/json");
|
|
1730
|
+
let requestBody;
|
|
1731
|
+
try {
|
|
1732
|
+
requestBody = await request.json();
|
|
1733
|
+
} catch {
|
|
1734
|
+
throw ActionError.badRequest("Request body must be valid JSON");
|
|
1735
|
+
}
|
|
1736
|
+
const parseResult = schema$1.safeParse(requestBody);
|
|
1737
|
+
if (!parseResult.success) throw ActionError.validationFailed("Invalid request body format", parseResult.error.issues);
|
|
1738
|
+
const url = new URL(request.url);
|
|
1739
|
+
const subscriptionId = url.searchParams.get("subscriptionId");
|
|
1740
|
+
const userId = url.searchParams.get("userId");
|
|
1741
|
+
if (!subscriptionId || !userId) throw ActionError.badRequest("Missing subscriptionId or userId");
|
|
1742
|
+
const reasonIds = parseResult.data.reason_ids ? parseResult.data.reason_ids.map((id) => parseInt(id, 10)) : void 0;
|
|
1743
|
+
const result = await this.api.subscription.cancel(subscriptionId, parseResult.data.feedback, reasonIds);
|
|
1744
|
+
if (!result) throw ActionError.internalError("Failed to cancel the subscription");
|
|
1745
|
+
return Response.json(result, { status: 200 });
|
|
1147
1746
|
}
|
|
1148
1747
|
};
|
|
1149
1748
|
|
|
1150
1749
|
//#endregion
|
|
1151
|
-
//#region src/
|
|
1152
|
-
|
|
1153
|
-
|
|
1750
|
+
//#region src/customer-portal/SubscriptionRenewalCouponAction.ts
|
|
1751
|
+
const schema = zod.object({ couponId: zod.string().min(1) });
|
|
1752
|
+
var SubscriptionRenewalCouponAction = class {
|
|
1753
|
+
actionName = "subscription_cancellation_coupon";
|
|
1754
|
+
constructor(api, auth) {
|
|
1154
1755
|
this.api = api;
|
|
1756
|
+
this.auth = auth;
|
|
1757
|
+
}
|
|
1758
|
+
createAction(id) {
|
|
1759
|
+
return `renewal_coupon_${id}`;
|
|
1760
|
+
}
|
|
1761
|
+
createAuthenticatedUrl(id, userId, endpoint) {
|
|
1762
|
+
const token = this.auth.createActionToken(this.createAction(id), userId);
|
|
1763
|
+
const url = new URL(endpoint);
|
|
1764
|
+
url.searchParams.set("action", this.actionName);
|
|
1765
|
+
url.searchParams.set("token", token);
|
|
1766
|
+
url.searchParams.set("subscriptionId", id);
|
|
1767
|
+
url.searchParams.set("userId", idToString(userId));
|
|
1768
|
+
return url.href;
|
|
1155
1769
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1770
|
+
verifyAuthentication(request) {
|
|
1771
|
+
const url = new URL(request.url);
|
|
1772
|
+
const action = url.searchParams.get("action");
|
|
1773
|
+
const token = url.searchParams.get("token");
|
|
1774
|
+
const subscriptionId = url.searchParams.get("subscriptionId");
|
|
1775
|
+
const userId = url.searchParams.get("userId");
|
|
1776
|
+
if (!action || !token || !subscriptionId || !userId) return false;
|
|
1777
|
+
return this.auth.verifyActionToken(token, this.createAction(subscriptionId), userId);
|
|
1778
|
+
}
|
|
1779
|
+
canHandle(request) {
|
|
1780
|
+
const url = new URL(request.url);
|
|
1781
|
+
const action = url.searchParams.get("action");
|
|
1782
|
+
return action === this.actionName && request.method === "POST";
|
|
1783
|
+
}
|
|
1784
|
+
async processAction(request) {
|
|
1785
|
+
const contentType = request.headers.get("content-type");
|
|
1786
|
+
if (!contentType || !contentType.includes("application/json")) throw ActionError.badRequest("Invalid content type. Expected application/json");
|
|
1787
|
+
let requestBody;
|
|
1788
|
+
try {
|
|
1789
|
+
requestBody = await request.json();
|
|
1790
|
+
} catch {
|
|
1791
|
+
throw ActionError.badRequest("Request body must be valid JSON");
|
|
1792
|
+
}
|
|
1793
|
+
const parseResult = schema.safeParse(requestBody);
|
|
1794
|
+
if (!parseResult.success) throw ActionError.validationFailed("Invalid request body format", parseResult.error.issues);
|
|
1795
|
+
const url = new URL(request.url);
|
|
1796
|
+
const subscriptionId = url.searchParams.get("subscriptionId");
|
|
1797
|
+
if (!subscriptionId) throw ActionError.badRequest("Missing subscriptionId in the request URL");
|
|
1798
|
+
const result = await this.api.subscription.applyRenewalCoupon(subscriptionId, parseResult.data.couponId, true);
|
|
1799
|
+
if (!result) throw ActionError.internalError("Failed to apply renewal coupon to the subscription");
|
|
1800
|
+
return Response.json(result, { status: 200 });
|
|
1168
1801
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
//#endregion
|
|
1805
|
+
//#region src/customer-portal/CustomerPortalActionService.ts
|
|
1806
|
+
var CustomerPortalActionService = class {
|
|
1807
|
+
invoice;
|
|
1808
|
+
billing;
|
|
1809
|
+
renewalCoupon;
|
|
1810
|
+
cancelRenewal;
|
|
1811
|
+
constructor(api, authService) {
|
|
1812
|
+
this.api = api;
|
|
1813
|
+
this.authService = authService;
|
|
1814
|
+
this.invoice = new InvoiceAction(this.api, this.authService);
|
|
1815
|
+
this.billing = new BillingAction(this.api, this.authService);
|
|
1816
|
+
this.renewalCoupon = new SubscriptionRenewalCouponAction(this.api, this.authService);
|
|
1817
|
+
this.cancelRenewal = new SubscriptionCancellationAction(this.api, this.authService);
|
|
1818
|
+
}
|
|
1819
|
+
getAllHandlers() {
|
|
1820
|
+
return [
|
|
1821
|
+
this.invoice,
|
|
1822
|
+
this.billing,
|
|
1823
|
+
this.renewalCoupon,
|
|
1824
|
+
this.cancelRenewal
|
|
1825
|
+
];
|
|
1178
1826
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1827
|
+
};
|
|
1828
|
+
|
|
1829
|
+
//#endregion
|
|
1830
|
+
//#region src/services/CustomerPortalService.ts
|
|
1831
|
+
var CustomerPortalService = class {
|
|
1832
|
+
repository;
|
|
1833
|
+
action;
|
|
1834
|
+
request;
|
|
1835
|
+
constructor(api, checkout, authService, purchase) {
|
|
1836
|
+
this.api = api;
|
|
1837
|
+
this.checkout = checkout;
|
|
1838
|
+
this.authService = authService;
|
|
1839
|
+
this.purchase = purchase;
|
|
1840
|
+
this.action = new CustomerPortalActionService(this.api, this.authService);
|
|
1841
|
+
this.repository = new PortalDataRepository(this.api, this.action, this.checkout);
|
|
1842
|
+
this.request = new PortalRequestProcessor(this.repository, this.action, this.purchase);
|
|
1193
1843
|
}
|
|
1194
1844
|
/**
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
* This is a convenience method that returns the purchase data in a format suitable for client-side rendering or serialization.
|
|
1845
|
+
* Retrieves the customer portal data for a user, including subscriptions, billing, and payments.
|
|
1198
1846
|
*/
|
|
1199
|
-
async
|
|
1200
|
-
|
|
1201
|
-
return purchaseInfos.map((info) => info.toData());
|
|
1847
|
+
async retrieveData(option) {
|
|
1848
|
+
return this.repository.retrievePortalDataByUserId(option);
|
|
1202
1849
|
}
|
|
1203
|
-
async
|
|
1204
|
-
|
|
1205
|
-
const license = await this.api.license.retrieve(subscription.license_id);
|
|
1206
|
-
if (!license) return null;
|
|
1207
|
-
const user = subscriptionUser && isIdsEqual(subscriptionUser.id, license.user_id) ? subscriptionUser : await this.api.user.retrieve(license.user_id);
|
|
1208
|
-
if (!user) return null;
|
|
1209
|
-
return new PurchaseInfo(user, license, subscription);
|
|
1850
|
+
async retrieveDataByEmail(option) {
|
|
1851
|
+
return this.repository.retrievePortalDataByEmail(option);
|
|
1210
1852
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1853
|
+
/**
|
|
1854
|
+
* Creates a restorer function that processes an array of purchases by invoking the provided callback for each purchase.
|
|
1855
|
+
*/
|
|
1856
|
+
createRestorer(callback) {
|
|
1857
|
+
return async (purchases) => {
|
|
1858
|
+
await Promise.all(purchases.map((purchase) => callback(purchase)));
|
|
1859
|
+
};
|
|
1215
1860
|
}
|
|
1216
1861
|
};
|
|
1217
1862
|
|
|
@@ -1224,25 +1869,28 @@ var WebhookListener = class {
|
|
|
1224
1869
|
this.secretKey = secretKey;
|
|
1225
1870
|
this.onError = onError;
|
|
1226
1871
|
}
|
|
1227
|
-
on(
|
|
1228
|
-
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const currentHandlers = this.eventHandlers.get(type);
|
|
1235
|
-
if (!currentHandlers) return this;
|
|
1236
|
-
currentHandlers.delete(handler);
|
|
1237
|
-
if (currentHandlers.size === 0) this.eventHandlers.delete(type);
|
|
1872
|
+
on(typeOrTypes, handler) {
|
|
1873
|
+
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
|
|
1874
|
+
for (const type of types) {
|
|
1875
|
+
if (!this.eventHandlers.has(type)) this.eventHandlers.set(type, /* @__PURE__ */ new Set());
|
|
1876
|
+
const existingHandlers = this.eventHandlers.get(type);
|
|
1877
|
+
existingHandlers?.add(handler);
|
|
1878
|
+
}
|
|
1238
1879
|
return this;
|
|
1239
1880
|
}
|
|
1240
|
-
|
|
1241
|
-
|
|
1881
|
+
off(typeOrTypes, handler) {
|
|
1882
|
+
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
|
|
1883
|
+
for (const type of types) {
|
|
1884
|
+
const currentHandlers = this.eventHandlers.get(type);
|
|
1885
|
+
if (!currentHandlers) continue;
|
|
1886
|
+
currentHandlers.delete(handler);
|
|
1887
|
+
if (currentHandlers.size === 0) this.eventHandlers.delete(type);
|
|
1888
|
+
}
|
|
1242
1889
|
return this;
|
|
1243
1890
|
}
|
|
1244
|
-
removeAll(
|
|
1245
|
-
|
|
1891
|
+
removeAll(typeOrTypes) {
|
|
1892
|
+
const types = Array.isArray(typeOrTypes) ? typeOrTypes : [typeOrTypes];
|
|
1893
|
+
for (const type of types) this.eventHandlers.delete(type);
|
|
1246
1894
|
return this;
|
|
1247
1895
|
}
|
|
1248
1896
|
getHandlerCount(type) {
|
|
@@ -1350,6 +1998,9 @@ var WebhookService = class {
|
|
|
1350
1998
|
createListener(onError) {
|
|
1351
1999
|
return new WebhookListener(this.secretKey, onError);
|
|
1352
2000
|
}
|
|
2001
|
+
createRequestProcessor(listener) {
|
|
2002
|
+
return (request) => this.processFetch(listener, request);
|
|
2003
|
+
}
|
|
1353
2004
|
/**
|
|
1354
2005
|
* WHATWG Fetch API adapter for modern JavaScript environments.
|
|
1355
2006
|
*
|
|
@@ -1430,6 +2081,406 @@ var WebhookService = class {
|
|
|
1430
2081
|
}
|
|
1431
2082
|
};
|
|
1432
2083
|
|
|
2084
|
+
//#endregion
|
|
2085
|
+
//#region src/services/AuthService.ts
|
|
2086
|
+
var AuthService = class AuthService {
|
|
2087
|
+
static TOKEN_SEPARATOR = "::";
|
|
2088
|
+
static DEFAULT_EXPIRY_MINUTES = 60;
|
|
2089
|
+
constructor(productId, secretKey) {
|
|
2090
|
+
this.productId = productId;
|
|
2091
|
+
this.secretKey = secretKey;
|
|
2092
|
+
if (!secretKey || secretKey.length < 32) throw new Error("Secret key must be at least 32 characters long");
|
|
2093
|
+
}
|
|
2094
|
+
/**
|
|
2095
|
+
* Creates a secure token for a specific action that can be performed by a user.
|
|
2096
|
+
*
|
|
2097
|
+
* @param action The action identifier (e.g., 'download_invoice', 'update_billing')
|
|
2098
|
+
* @param userId The ID of the user who can perform this action
|
|
2099
|
+
* @param expiryMinutes Optional expiry time in minutes (default: 60 minutes)
|
|
2100
|
+
* @returns A secure token string
|
|
2101
|
+
*/
|
|
2102
|
+
createActionToken(action, userId, expiryMinutes = AuthService.DEFAULT_EXPIRY_MINUTES) {
|
|
2103
|
+
if (!action || action.trim().length === 0) throw new Error("Action cannot be empty");
|
|
2104
|
+
const now = Date.now();
|
|
2105
|
+
const expiresAt = now + expiryMinutes * 60 * 1e3;
|
|
2106
|
+
const nonce = (0, crypto.randomBytes)(16).toString("hex");
|
|
2107
|
+
const payload = this.encodeTokenPayload({
|
|
2108
|
+
expiresAt,
|
|
2109
|
+
nonce
|
|
2110
|
+
});
|
|
2111
|
+
const signature = this.signData(action.trim(), userId, expiresAt, nonce);
|
|
2112
|
+
return `${payload}${AuthService.TOKEN_SEPARATOR}${signature}`;
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Verifies and validates an action token.
|
|
2116
|
+
*
|
|
2117
|
+
* @param token The token to verify
|
|
2118
|
+
* @param action The expected action
|
|
2119
|
+
* @param userId The expected user ID
|
|
2120
|
+
* @returns true if valid, false otherwise
|
|
2121
|
+
*/
|
|
2122
|
+
verifyActionToken(token, action, userId) {
|
|
2123
|
+
try {
|
|
2124
|
+
if (!token || typeof token !== "string" || !action || !userId) return false;
|
|
2125
|
+
const parts = token.split(AuthService.TOKEN_SEPARATOR);
|
|
2126
|
+
if (parts.length !== 2) return false;
|
|
2127
|
+
const [payloadPart, signature] = parts;
|
|
2128
|
+
if (!payloadPart || !signature) return false;
|
|
2129
|
+
const payload = this.decodeTokenPayload(payloadPart);
|
|
2130
|
+
if (!payload) return false;
|
|
2131
|
+
const now = Date.now();
|
|
2132
|
+
if (payload.expiresAt <= now) return false;
|
|
2133
|
+
const expectedSignature = this.signData(action.trim(), userId, payload.expiresAt, payload.nonce);
|
|
2134
|
+
return this.constantTimeEqual(signature, expectedSignature);
|
|
2135
|
+
} catch {
|
|
2136
|
+
return false;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
encodeTokenPayload(payload) {
|
|
2140
|
+
const jsonString = JSON.stringify(payload);
|
|
2141
|
+
return Buffer.from(jsonString).toString("base64url");
|
|
2142
|
+
}
|
|
2143
|
+
decodeTokenPayload(payloadPart) {
|
|
2144
|
+
try {
|
|
2145
|
+
const jsonString = Buffer.from(payloadPart, "base64url").toString("utf8");
|
|
2146
|
+
const data = JSON.parse(jsonString);
|
|
2147
|
+
if (typeof data.expiresAt !== "number" || !data.nonce) return null;
|
|
2148
|
+
return data;
|
|
2149
|
+
} catch {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
signData(action, userId, expiresAt, nonce) {
|
|
2154
|
+
const data = `${action}:${idToString(userId)}:${idToString(this.productId)}:${expiresAt}:${nonce}`;
|
|
2155
|
+
return (0, crypto.createHmac)("sha256", this.secretKey).update(data).digest("hex");
|
|
2156
|
+
}
|
|
2157
|
+
constantTimeEqual(a, b) {
|
|
2158
|
+
if (a.length !== b.length) return false;
|
|
2159
|
+
try {
|
|
2160
|
+
return (0, crypto.timingSafeEqual)(Buffer.from(a), Buffer.from(b));
|
|
2161
|
+
} catch {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
2166
|
+
|
|
2167
|
+
//#endregion
|
|
2168
|
+
//#region src/services/PricingService.ts
|
|
2169
|
+
var PricingService = class {
|
|
2170
|
+
constructor(api) {
|
|
2171
|
+
this.api = api;
|
|
2172
|
+
}
|
|
2173
|
+
async retrieve(topupPlanId) {
|
|
2174
|
+
const pricingData = await this.api.product.retrievePricingData();
|
|
2175
|
+
const plans = pricingData?.plans?.filter((plan) => {
|
|
2176
|
+
return plan.pricing?.some((p) => this.isValidPricing(p));
|
|
2177
|
+
}).map((plan) => {
|
|
2178
|
+
return {
|
|
2179
|
+
...plan,
|
|
2180
|
+
pricing: plan.pricing?.filter((p) => this.isValidPricing(p)) ?? []
|
|
2181
|
+
};
|
|
2182
|
+
}) ?? [];
|
|
2183
|
+
return {
|
|
2184
|
+
plans,
|
|
2185
|
+
topupPlan: this.findTopupPlan(plans, topupPlanId),
|
|
2186
|
+
sellingUnit: pricingData?.plugin?.selling_unit_label ?? {
|
|
2187
|
+
singular: "Unit",
|
|
2188
|
+
plural: "Units"
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
findTopupPlan(plans, planId) {
|
|
2193
|
+
if (!plans) return null;
|
|
2194
|
+
const topupPlan = plans.find((plan) => {
|
|
2195
|
+
return isIdsEqual(plan.id, planId ?? "") && !plan.is_hidden || plan.pricing?.filter((p) => this.isValidPricing(p)).every((p) => p.lifetime_price);
|
|
2196
|
+
});
|
|
2197
|
+
return topupPlan ?? null;
|
|
2198
|
+
}
|
|
2199
|
+
isValidPricing(pricing) {
|
|
2200
|
+
return !!(pricing.monthly_price || pricing.annual_price || pricing.lifetime_price);
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
|
|
2204
|
+
//#endregion
|
|
2205
|
+
//#region src/models/PurchaseInfo.ts
|
|
2206
|
+
var PurchaseInfo = class {
|
|
2207
|
+
email;
|
|
2208
|
+
firstName;
|
|
2209
|
+
lastName;
|
|
2210
|
+
userId;
|
|
2211
|
+
planId;
|
|
2212
|
+
pricingId;
|
|
2213
|
+
licenseId;
|
|
2214
|
+
expiration;
|
|
2215
|
+
canceled;
|
|
2216
|
+
subscriptionId;
|
|
2217
|
+
billingCycle;
|
|
2218
|
+
quota;
|
|
2219
|
+
initialAmount;
|
|
2220
|
+
renewalAmount;
|
|
2221
|
+
currency;
|
|
2222
|
+
renewalDate;
|
|
2223
|
+
paymentMethod;
|
|
2224
|
+
created;
|
|
2225
|
+
constructor(user, license, subscription) {
|
|
2226
|
+
this.email = user.email;
|
|
2227
|
+
this.firstName = user.first ?? "";
|
|
2228
|
+
this.lastName = user.last ?? "";
|
|
2229
|
+
this.userId = idToString(license.user_id);
|
|
2230
|
+
this.canceled = license.is_cancelled ?? false;
|
|
2231
|
+
this.expiration = license.expiration ? parseDateTime(license.expiration) : null;
|
|
2232
|
+
this.licenseId = idToString(license.id);
|
|
2233
|
+
this.planId = idToString(license.plan_id);
|
|
2234
|
+
this.subscriptionId = null;
|
|
2235
|
+
this.billingCycle = null;
|
|
2236
|
+
this.quota = license.quota ?? null;
|
|
2237
|
+
this.pricingId = idToString(license.pricing_id);
|
|
2238
|
+
this.initialAmount = null;
|
|
2239
|
+
this.renewalAmount = null;
|
|
2240
|
+
this.currency = null;
|
|
2241
|
+
this.renewalDate = null;
|
|
2242
|
+
this.paymentMethod = null;
|
|
2243
|
+
this.created = parseDateTime(license.created) ?? /* @__PURE__ */ new Date();
|
|
2244
|
+
if (subscription) {
|
|
2245
|
+
this.subscriptionId = idToString(subscription.id);
|
|
2246
|
+
this.billingCycle = parseBillingCycle(subscription.billing_cycle);
|
|
2247
|
+
this.initialAmount = parseNumber(subscription.initial_amount);
|
|
2248
|
+
this.renewalAmount = parseNumber(subscription.renewal_amount);
|
|
2249
|
+
this.currency = parseCurrency(subscription.currency);
|
|
2250
|
+
this.renewalDate = subscription.next_payment ? parseDateTime(subscription.next_payment) : null;
|
|
2251
|
+
this.paymentMethod = parsePaymentMethod(subscription.gateway);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
isPlan(planId) {
|
|
2255
|
+
return this.planId === idToString(planId);
|
|
2256
|
+
}
|
|
2257
|
+
isFromPlans(planIds) {
|
|
2258
|
+
return planIds.some((planId) => this.isPlan(planId));
|
|
2259
|
+
}
|
|
2260
|
+
toData() {
|
|
2261
|
+
return {
|
|
2262
|
+
email: this.email,
|
|
2263
|
+
firstName: this.firstName,
|
|
2264
|
+
lastName: this.lastName,
|
|
2265
|
+
userId: this.userId,
|
|
2266
|
+
planId: this.planId,
|
|
2267
|
+
pricingId: this.pricingId,
|
|
2268
|
+
licenseId: this.licenseId,
|
|
2269
|
+
expiration: this.expiration,
|
|
2270
|
+
canceled: this.canceled,
|
|
2271
|
+
subscriptionId: this.subscriptionId,
|
|
2272
|
+
billingCycle: this.billingCycle,
|
|
2273
|
+
quota: this.quota,
|
|
2274
|
+
isActive: this.isActive,
|
|
2275
|
+
initialAmount: this.initialAmount,
|
|
2276
|
+
renewalAmount: this.renewalAmount,
|
|
2277
|
+
currency: this.currency,
|
|
2278
|
+
renewalDate: this.renewalDate,
|
|
2279
|
+
paymentMethod: this.paymentMethod,
|
|
2280
|
+
created: this.created
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* A convenience method to convert the purchase info to a format suitable for database storage.
|
|
2285
|
+
*/
|
|
2286
|
+
toEntitlementRecord(additionalData = {}) {
|
|
2287
|
+
return {
|
|
2288
|
+
...additionalData,
|
|
2289
|
+
fsLicenseId: this.licenseId,
|
|
2290
|
+
fsPlanId: this.planId,
|
|
2291
|
+
fsPricingId: this.pricingId,
|
|
2292
|
+
fsUserId: this.userId,
|
|
2293
|
+
type: this.isSubscription() ? "subscription" : "oneoff",
|
|
2294
|
+
expiration: this.expiration,
|
|
2295
|
+
createdAt: this.created,
|
|
2296
|
+
isCanceled: this.canceled
|
|
2297
|
+
};
|
|
2298
|
+
}
|
|
2299
|
+
get isActive() {
|
|
2300
|
+
if (this.canceled) return false;
|
|
2301
|
+
if (this.expiration && this.expiration < /* @__PURE__ */ new Date()) return false;
|
|
2302
|
+
return true;
|
|
2303
|
+
}
|
|
2304
|
+
isSubscription() {
|
|
2305
|
+
return this.subscriptionId !== null;
|
|
2306
|
+
}
|
|
2307
|
+
isAnnual() {
|
|
2308
|
+
return this.billingCycle === BILLING_CYCLE.YEARLY;
|
|
2309
|
+
}
|
|
2310
|
+
isMonthly() {
|
|
2311
|
+
return this.billingCycle === BILLING_CYCLE.MONTHLY;
|
|
2312
|
+
}
|
|
2313
|
+
isOneOff() {
|
|
2314
|
+
return this.billingCycle === BILLING_CYCLE.ONEOFF || this.billingCycle === null;
|
|
2315
|
+
}
|
|
2316
|
+
getPlanTitle(pricingData) {
|
|
2317
|
+
const plan = pricingData?.plans?.find((p) => isIdsEqual(p.id, this.planId));
|
|
2318
|
+
return plan?.title ?? plan?.name ?? "Deleted Plan";
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
|
|
2322
|
+
//#endregion
|
|
2323
|
+
//#region src/services/PurchaseService.ts
|
|
2324
|
+
var PurchaseService = class {
|
|
2325
|
+
constructor(api) {
|
|
2326
|
+
this.api = api;
|
|
2327
|
+
}
|
|
2328
|
+
/**
|
|
2329
|
+
* Retrieve purchase information from the Freemius API based on the license ID.
|
|
2330
|
+
*
|
|
2331
|
+
* The license is the primary entitlement for a purchase, and it may or may not be associated with a subscription.
|
|
2332
|
+
* With this method, you can retrieve detailed information about the purchase, including user details, plan, expiration, and more.
|
|
2333
|
+
*/
|
|
2334
|
+
async retrievePurchase(licenseId) {
|
|
2335
|
+
const [license, subscription] = await Promise.all([await this.api.license.retrieve(licenseId), await this.api.license.retrieveSubscription(licenseId)]);
|
|
2336
|
+
if (!license) return null;
|
|
2337
|
+
const user = await this.api.user.retrieve(license.user_id);
|
|
2338
|
+
if (!user) return null;
|
|
2339
|
+
return new PurchaseInfo(user, license, subscription);
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* A helper method to retrieve raw purchase data instead of a full PurchaseInfo object.
|
|
2343
|
+
*
|
|
2344
|
+
* This is useful when passing data from server to client in frameworks like Next.js, where only serializable data should be sent.
|
|
2345
|
+
*/
|
|
2346
|
+
async retrievePurchaseData(licenseId) {
|
|
2347
|
+
const purchaseInfo = await this.retrievePurchase(licenseId);
|
|
2348
|
+
if (!purchaseInfo) return null;
|
|
2349
|
+
return purchaseInfo.toData();
|
|
2350
|
+
}
|
|
2351
|
+
async retrievePurchases(userOrEntity, pagination) {
|
|
2352
|
+
const user = typeof userOrEntity === "object" ? userOrEntity : await this.api.user.retrieve(userOrEntity);
|
|
2353
|
+
if (!user) return [];
|
|
2354
|
+
const licenses = await this.api.user.retrieveLicenses(user.id, { type: "active" }, pagination);
|
|
2355
|
+
if (!licenses || !licenses.length) return [];
|
|
2356
|
+
const licenseSubscriptionPromises = licenses.map(async (license) => {
|
|
2357
|
+
const subscription = license.expiration !== null ? await this.api.license.retrieveSubscription(license.id) : null;
|
|
2358
|
+
return new PurchaseInfo(user, license, subscription);
|
|
2359
|
+
});
|
|
2360
|
+
return await Promise.all(licenseSubscriptionPromises).then((results) => results.filter((result) => result !== null).sort((a, b) => b.created.getTime() - a.created.getTime()));
|
|
2361
|
+
}
|
|
2362
|
+
async retrievePurchasesData(userOrEntity, pagination) {
|
|
2363
|
+
const purchaseInfos = await this.retrievePurchases(userOrEntity, pagination);
|
|
2364
|
+
return purchaseInfos.map((info) => info.toData());
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Retrieve a list of active subscriptions for a user. You can use this method to find or sync subscriptions from freemius to your system.
|
|
2368
|
+
*/
|
|
2369
|
+
async retrieveSubscriptions(userOrEntity, pagination) {
|
|
2370
|
+
const user = typeof userOrEntity === "object" ? userOrEntity : await this.api.user.retrieve(userOrEntity);
|
|
2371
|
+
if (!user) return [];
|
|
2372
|
+
const subscriptions = await this.api.user.retrieveSubscriptions(user.id, { filter: "active" }, pagination);
|
|
2373
|
+
if (!subscriptions || !subscriptions.length) return [];
|
|
2374
|
+
const licenseSubscriptionPromises = subscriptions.map(async (subscription) => {
|
|
2375
|
+
const license = await this.api.license.retrieve(subscription.license_id);
|
|
2376
|
+
if (!license) return null;
|
|
2377
|
+
return new PurchaseInfo(user, license, subscription);
|
|
2378
|
+
});
|
|
2379
|
+
return await Promise.all(licenseSubscriptionPromises).then((results) => results.filter((result) => result !== null).sort((a, b) => b.created.getTime() - a.created.getTime()));
|
|
2380
|
+
}
|
|
2381
|
+
/**
|
|
2382
|
+
* Retrieve a list of purchase data for a user.
|
|
2383
|
+
*
|
|
2384
|
+
* This is a convenience method that returns the purchase data in a format suitable for client-side rendering or serialization.
|
|
2385
|
+
*/
|
|
2386
|
+
async retrieveSubscriptionsData(userId, pagination) {
|
|
2387
|
+
const purchaseInfos = await this.retrieveSubscriptions(userId, pagination);
|
|
2388
|
+
return purchaseInfos.map((info) => info.toData());
|
|
2389
|
+
}
|
|
2390
|
+
async retrieveBySubscription(subscription, subscriptionUser) {
|
|
2391
|
+
if (!subscription.license_id) return null;
|
|
2392
|
+
const license = await this.api.license.retrieve(subscription.license_id);
|
|
2393
|
+
if (!license) return null;
|
|
2394
|
+
const user = subscriptionUser && isIdsEqual(subscriptionUser.id, license.user_id) ? subscriptionUser : await this.api.user.retrieve(license.user_id);
|
|
2395
|
+
if (!user) return null;
|
|
2396
|
+
return new PurchaseInfo(user, license, subscription);
|
|
2397
|
+
}
|
|
2398
|
+
async retrieveSubscriptionsByEmail(email, pagination) {
|
|
2399
|
+
const user = await this.api.user.retrieveByEmail(email);
|
|
2400
|
+
if (!user) return [];
|
|
2401
|
+
return await this.retrieveSubscriptions(user.id, pagination);
|
|
2402
|
+
}
|
|
2403
|
+
async retrievePurchasesByEmail(email, pagination) {
|
|
2404
|
+
const user = await this.api.user.retrieveByEmail(email);
|
|
2405
|
+
if (!user) return [];
|
|
2406
|
+
return await this.retrievePurchases(user.id, pagination);
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
|
|
2410
|
+
//#endregion
|
|
2411
|
+
//#region src/services/EntitlementService.ts
|
|
2412
|
+
var EntitlementService = class {
|
|
2413
|
+
/**
|
|
2414
|
+
* Get the active subscription entitlement from a list of entitlements stored in your own database.
|
|
2415
|
+
*
|
|
2416
|
+
* @param entitlements - Array of entitlements to filter
|
|
2417
|
+
* @returns The single active entitlement, or null if none found
|
|
2418
|
+
* @throws Error if multiple active entitlements are found
|
|
2419
|
+
*/
|
|
2420
|
+
getActive(entitlements) {
|
|
2421
|
+
const activeEntitlements = this.getActives(entitlements);
|
|
2422
|
+
if (!activeEntitlements || activeEntitlements.length === 0) return null;
|
|
2423
|
+
if (activeEntitlements.length > 1) throw new Error(`Multiple active entitlements found: ${activeEntitlements.length} entitlements are active`);
|
|
2424
|
+
return activeEntitlements[0];
|
|
2425
|
+
}
|
|
2426
|
+
/**
|
|
2427
|
+
* Get all active subscription entitlements from a list of entitlements stored in your own database.
|
|
2428
|
+
*
|
|
2429
|
+
* @param entitlements - Array of entitlements to filter
|
|
2430
|
+
* @returns Array of active entitlements, or null if none found
|
|
2431
|
+
*/
|
|
2432
|
+
getActives(entitlements) {
|
|
2433
|
+
if (!entitlements || entitlements.length === 0) return null;
|
|
2434
|
+
const activeEntitlements = entitlements.filter((entitlement) => {
|
|
2435
|
+
if (entitlement.type !== "subscription") return false;
|
|
2436
|
+
if (entitlement.isCanceled) return false;
|
|
2437
|
+
if (entitlement.expiration === null) return true;
|
|
2438
|
+
const expiration = entitlement.expiration instanceof Date ? entitlement.expiration : parseDateTime(entitlement.expiration);
|
|
2439
|
+
if (expiration && expiration < /* @__PURE__ */ new Date()) return false;
|
|
2440
|
+
return true;
|
|
2441
|
+
});
|
|
2442
|
+
return activeEntitlements.length > 0 ? activeEntitlements : null;
|
|
2443
|
+
}
|
|
2444
|
+
getFsUser(entitlement, email) {
|
|
2445
|
+
if (entitlement) return {
|
|
2446
|
+
email,
|
|
2447
|
+
id: entitlement.fsUserId,
|
|
2448
|
+
primaryLicenseId: entitlement.fsLicenseId
|
|
2449
|
+
};
|
|
2450
|
+
return email ? { email } : null;
|
|
2451
|
+
}
|
|
2452
|
+
/**
|
|
2453
|
+
* Calculates the number of complete months elapsed since the entitlement subscription was created.
|
|
2454
|
+
*
|
|
2455
|
+
* @param entitlement - The entitlement to check
|
|
2456
|
+
* @returns Number of complete months elapsed, or -1 if entitlement is null
|
|
2457
|
+
*/
|
|
2458
|
+
getElapsedMonth(entitlement) {
|
|
2459
|
+
if (!entitlement) return -1;
|
|
2460
|
+
const created = entitlement.createdAt instanceof Date ? entitlement.createdAt : parseDateTime(entitlement.createdAt);
|
|
2461
|
+
if (!created) return -1;
|
|
2462
|
+
const now = /* @__PURE__ */ new Date();
|
|
2463
|
+
let months = (now.getFullYear() - created.getFullYear()) * 12 + (now.getMonth() - created.getMonth());
|
|
2464
|
+
if (now.getDate() < created.getDate()) months -= 1;
|
|
2465
|
+
return months;
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Calculates the number of complete years elapsed since the entitlement subscription was created.
|
|
2469
|
+
*
|
|
2470
|
+
* @param entitlement - The entitlement to check
|
|
2471
|
+
* @returns Number of complete years elapsed, or -1 if entitlement is null
|
|
2472
|
+
*/
|
|
2473
|
+
getElapsedYear(entitlement) {
|
|
2474
|
+
if (!entitlement) return -1;
|
|
2475
|
+
const created = entitlement.createdAt instanceof Date ? entitlement.createdAt : parseDateTime(entitlement.createdAt);
|
|
2476
|
+
if (!created) return -1;
|
|
2477
|
+
const now = /* @__PURE__ */ new Date();
|
|
2478
|
+
let years = now.getFullYear() - created.getFullYear();
|
|
2479
|
+
if (now.getMonth() < created.getMonth() || now.getMonth() === created.getMonth() && now.getDate() < created.getDate()) years -= 1;
|
|
2480
|
+
return years;
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
|
|
1433
2484
|
//#endregion
|
|
1434
2485
|
//#region src/Freemius.ts
|
|
1435
2486
|
var Freemius = class {
|
|
@@ -1438,11 +2489,17 @@ var Freemius = class {
|
|
|
1438
2489
|
purchase;
|
|
1439
2490
|
customerPortal;
|
|
1440
2491
|
webhook;
|
|
1441
|
-
|
|
2492
|
+
pricing;
|
|
2493
|
+
entitlement = new EntitlementService();
|
|
2494
|
+
auth;
|
|
2495
|
+
constructor(config) {
|
|
2496
|
+
const { productId, apiKey, secretKey, publicKey } = config;
|
|
1442
2497
|
this.api = new ApiService(productId, apiKey, secretKey, publicKey);
|
|
1443
|
-
this.
|
|
2498
|
+
this.auth = new AuthService(productId, secretKey);
|
|
2499
|
+
this.pricing = new PricingService(this.api);
|
|
1444
2500
|
this.purchase = new PurchaseService(this.api);
|
|
1445
|
-
this.
|
|
2501
|
+
this.checkout = new CheckoutService(productId, publicKey, secretKey, this.purchase, this.pricing);
|
|
2502
|
+
this.customerPortal = new CustomerPortalService(this.api, this.checkout, this.auth, this.purchase);
|
|
1446
2503
|
this.webhook = new WebhookService(secretKey);
|
|
1447
2504
|
}
|
|
1448
2505
|
};
|
|
@@ -1451,7 +2508,6 @@ var Freemius = class {
|
|
|
1451
2508
|
exports.BILLING_CYCLE = BILLING_CYCLE;
|
|
1452
2509
|
exports.CURRENCY = CURRENCY;
|
|
1453
2510
|
exports.Freemius = Freemius;
|
|
1454
|
-
exports.PurchaseInfo = PurchaseInfo;
|
|
1455
2511
|
exports.idToNumber = idToNumber;
|
|
1456
2512
|
exports.idToString = idToString;
|
|
1457
2513
|
exports.isIdsEqual = isIdsEqual;
|