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