@freemius/sdk 0.0.1 → 0.0.2

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/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.2";
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
- return createClient({
103
+ const platform = detectPlatform();
104
+ const client = createClient({
92
105
  baseUrl,
93
- headers: { Authorization: bearerToken ? `Bearer ${bearerToken}` : void 0 }
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 PAGING_DEFAULT_LIMIT = 150;
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.status !== 200 || !response.data) return null;
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.status !== 200 || !response.data) return null;
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 response = await this.client.GET(`/products/{product_id}/subscriptions/{subscription_id}.json`, { params: { path: {
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 (response.response.status !== 200 || !response.data || !response.data.id) return null;
218
- return response.data;
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 response = await this.client.GET(`/products/{product_id}/subscriptions.json`, { params: {
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 (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
229
- return response.data.subscriptions;
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 response = await this.client.GET(`/products/{product_id}/users/{user_id}/subscriptions.json`, { params: {
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 (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
288
- return response.data.subscriptions;
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
- const isTestServer = process.env.FS__INTERNAL__IS_DEVELOPMENT_MODE === "true";
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/utils/ops.ts
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 CheckoutBuilder = class CheckoutBuilder {
434
- constructor(options, productId, publicKey, secretKey) {
435
- this.options = options;
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
- inSandbox() {
446
- const timestamp = Math.floor(Date.now() / 1e3).toString();
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
- ctx: timestamp,
452
- token: createHash("md5").update(token).digest("hex")
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
- withUser(user, readonly = true) {
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
- return new CheckoutBuilder({
604
+ this.options = {
474
605
  ...this.options,
475
606
  user_email: user.email,
476
- user_first_name: firstName,
477
- user_last_name: lastName,
607
+ user_firstname: firstName,
608
+ user_lastname: lastName,
478
609
  readonly_user: readonly
479
- }, this.productId, this.publicKey, this.secretKey);
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
- withRecommendation() {
488
- return new CheckoutBuilder({
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
- }, this.productId, this.publicKey, this.secretKey);
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
- withPlan(planId) {
504
- return new CheckoutBuilder({
636
+ setPlan(planId) {
637
+ this.options = {
505
638
  ...this.options,
506
639
  plan_id: planId.toString()
507
- }, this.productId, this.publicKey, this.secretKey);
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
- withQuota(count) {
516
- return new CheckoutBuilder({
649
+ setQuota(count) {
650
+ this.options = {
517
651
  ...this.options,
518
652
  licenses: count
519
- }, this.productId, this.publicKey, this.secretKey);
653
+ };
654
+ return this;
520
655
  }
521
- withPricing(pricingId) {
522
- return new CheckoutBuilder({
656
+ setPricing(pricingId) {
657
+ this.options = {
523
658
  ...this.options,
524
659
  pricing_id: pricingId.toString()
525
- }, this.productId, this.publicKey, this.secretKey);
660
+ };
661
+ return this;
526
662
  }
527
- withTitle(title) {
528
- return new CheckoutBuilder({
663
+ setTitle(title) {
664
+ this.options = {
529
665
  ...this.options,
530
666
  title
531
- }, this.productId, this.publicKey, this.secretKey);
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
- withCoupon(options) {
677
+ setCoupon(options) {
541
678
  const { code: coupon, hideUI = false } = options;
542
- const newOptions = {
679
+ this.options = {
543
680
  ...this.options,
681
+ coupon,
544
682
  hide_coupon: hideUI
545
683
  };
546
- if (coupon !== void 0) newOptions.coupon = coupon;
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
- inTrial(mode = true) {
556
- return new CheckoutBuilder({
692
+ setTrial(mode = true) {
693
+ this.options = {
557
694
  ...this.options,
558
695
  trial: mode
559
- }, this.productId, this.publicKey, this.secretKey);
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
- withAppearance(options) {
568
- const newOptions = { ...this.options };
569
- if (options.layout !== void 0) newOptions.layout = options.layout;
570
- if (options.formPosition !== void 0) newOptions.form_position = options.formPosition;
571
- if (options.fullscreen !== void 0) newOptions.fullscreen = options.fullscreen;
572
- if (options.modalTitle !== void 0) newOptions.modal_title = options.modalTitle;
573
- if (options.id !== void 0) newOptions.id = options.id;
574
- return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
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
- withDiscounts(options) {
583
- const newOptions = { ...this.options };
584
- if (options.annual !== void 0) newOptions.annual_discount = options.annual;
585
- if (options.multisite !== void 0) newOptions.multisite_discount = options.multisite;
586
- if (options.bundle !== void 0) newOptions.bundle_discount = options.bundle;
587
- if (options.showMonthlySwitch !== void 0) newOptions.show_monthly_switch = options.showMonthlySwitch;
588
- return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
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
- withBillingCycle(defaultCycle, selector) {
598
- const newOptions = { ...this.options };
599
- if (selector !== void 0) newOptions.billing_cycle_selector = selector;
600
- if (defaultCycle !== void 0) newOptions.billing_cycle = defaultCycle;
601
- return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
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
- withLanguage(locale = "auto") {
610
- return new CheckoutBuilder({
747
+ setLanguage(locale = "auto") {
748
+ this.options = {
611
749
  ...this.options,
612
- language: locale,
613
- locale
614
- }, this.productId, this.publicKey, this.secretKey);
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
- withReviewsAndBadges(options) {
623
- const newOptions = { ...this.options };
624
- if (options.showReviews !== void 0) newOptions.show_reviews = options.showReviews;
625
- if (options.reviewId !== void 0) newOptions.review_id = options.reviewId;
626
- if (options.showRefundBadge !== void 0) newOptions.show_refund_badge = options.showRefundBadge;
627
- if (options.refundPolicyPosition !== void 0) newOptions.refund_policy_position = options.refundPolicyPosition;
628
- return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
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
- withCurrency(currency, defaultCurrency = "usd", showInlineSelector = true) {
639
- const options = {
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
- if (currency !== "auto") options.currency = currency;
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
- withNavigation(cancelUrl, cancelIcon) {
655
- const newOptions = { ...this.options };
656
- if (cancelUrl !== void 0) newOptions.cancel_url = cancelUrl;
657
- if (cancelIcon !== void 0) newOptions.cancel_icon = cancelIcon;
658
- return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
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
- withAffiliate(userId) {
667
- return new CheckoutBuilder({
804
+ setAffiliate(userId) {
805
+ this.options = {
668
806
  ...this.options,
669
807
  affiliate_user_id: userId
670
- }, this.productId, this.publicKey, this.secretKey);
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
- withImage(imageUrl) {
679
- return new CheckoutBuilder({
817
+ setImage(imageUrl) {
818
+ this.options = {
680
819
  ...this.options,
681
820
  image: imageUrl
682
- }, this.productId, this.publicKey, this.secretKey);
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
- forRenewal(licenseKey) {
691
- return new CheckoutBuilder({
830
+ setLicenseRenewal(licenseKey) {
831
+ this.options = {
692
832
  ...this.options,
693
833
  license_key: licenseKey
694
- }, this.productId, this.publicKey, this.secretKey);
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
- toOptions(additionalOptions) {
702
- return {
703
- ...this.options,
704
- ...additionalOptions,
705
- product_id: this.productId
706
- };
844
+ getOptions() {
845
+ return { ...this.options };
707
846
  }
708
- toLink() {
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(`https://checkout.freemius.com/product/${this.productId}/`);
855
+ const url = new URL(`${this.getBaseUrl()}/product/${this.productId}/`);
712
856
  url.search = queryParams;
713
- return url.href;
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
- constructor(productId, publicKey, secretKey) {
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 options = freemius.checkout.params()
796
- * .withUser(session?.user)
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 checkoutOptions = freemius.checkout.params()
806
- * .withUser(user, true)
807
- * .withPlan('1234')
808
- * .withQuota(5)
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
- * .inTrial('paid')
815
- * .withAppearance({
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
- * Useful for generating recommended checkout links for SaaS.
1186
+ * @example
858
1187
  */
859
- createUserLink(user, isSandbox = false) {
860
- let builder = this.create().withUser(user);
861
- if (isSandbox) builder = builder.inSandbox();
862
- return builder.toLink();
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
- const timestamp = Math.floor(Date.now() / 1e3).toString();
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 from Freemius Checkout.
1215
+ * Processes a redirect URL and returns the checkout redirect information if valid.
884
1216
  *
885
- * This method verifies the signature in the URL and returns a CheckoutRedirectInfo object if successful.
1217
+ * This is useful for handling redirects from the checkout portal back to your application.
886
1218
  *
887
- * For nextjs like applications, make sure to replace the URL from the `Request` object with the right hostname to take care of the proxy.
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
- * 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/...`.
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
- * ```ts
894
- * export async function GET(request: Request) {
895
- * // Replace the URL with the actual hostname of your application
896
- * // This is important for the signature verification to work correctly.
897
- * const data = await freemius.checkout.processRedirect(
898
- * request.url.replace('https://localhost:3000', 'https://xyz.ngrok-free.app')
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
- async processRedirect(currentUrl) {
904
- const url = new URL(currentUrl.replace(/%20/g, "+"));
905
- const signature = url.searchParams.get("signature");
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/services/CustomerPortalService.ts
927
- var CustomerPortalService = class {
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
- * Retrieves the customer portal data for a user, including subscriptions, billing, and payments.
934
- *
935
- * @param userId The ID of the user for whom to retrieve portal data.
936
- * @param primaryLicenseId Optional primary license ID to include in the portal data. If present then the `primary` field will be populated with related information which our `@freemius/saas-starter` package uses to display the primary purchase information.
937
- */
938
- async retrieveData(userId, primaryLicenseId = null, sandbox = false) {
939
- const [user, pricingData, subscriptions, payments, billing] = await Promise.all([
940
- this.api.user.retrieve(userId),
941
- this.api.product.retrievePricingData(),
942
- this.api.user.retrieveSubscriptions(userId),
943
- this.api.user.retrievePayments(userId),
944
- this.api.user.retrieveBilling(userId)
945
- ]);
946
- if (!user || !pricingData || !subscriptions) return null;
947
- const planTitles = this.getPlanTitleById(pricingData);
948
- const allPricingsById = this.getPricingById(pricingData);
949
- const portalPayments = payments.map((payment) => ({
950
- ...payment,
951
- invoiceUrl: this.api.getSignedUrl(this.api.createUrl(`payments/${payment.id}/invoice.pdf`)),
952
- paymentMethod: parsePaymentMethod(payment.gateway),
953
- createdAt: parseDateTime(payment.created) ?? /* @__PURE__ */ new Date(),
954
- planTitle: planTitles[payment.plan_id] ?? `Plan ${payment.plan_id}`,
955
- quota: allPricingsById[payment.pricing_id]?.licenses ?? null
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 billingData = {
1279
+ const portalData = {
1280
+ endpoint,
960
1281
  user,
961
1282
  checkoutOptions,
962
- billing,
963
- subscriptions: {
964
- primary: null,
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: planTitles[subscription.plan_id] ?? `Plan ${subscription.plan_id}`,
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: allPricingsById[subscription.pricing_id]?.licenses ?? null,
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) billingData.subscriptions.active.push(subscriptionData);
996
- else billingData.subscriptions.past.push(subscriptionData);
997
- if (isActive && primaryLicenseId && isIdsEqual(subscription.license_id, primaryLicenseId)) billingData.subscriptions.primary = subscriptionData;
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
- billingData.subscriptions.active.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1000
- billingData.subscriptions.past.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1001
- if (!billingData.subscriptions.primary) billingData.subscriptions.primary = billingData.subscriptions.active[0] ?? billingData.subscriptions.past[0] ?? null;
1002
- if (billingData.subscriptions.primary) billingData.subscriptions.primary.checkoutUpgradeAuthorization = await this.api.license.retrieveCheckoutUpgradeAuthorization(billingData.subscriptions.primary.licenseId);
1003
- return billingData;
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
- * @todo - Implement this method to handle actions like get cancel coupon, cancel subscription, update billing, get upgrade auth for Checkout etc.
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
- getPlanTitleById(pricingData) {
1009
- const planTitles = {};
1010
- pricingData.plans?.forEach((plan) => {
1011
- planTitles[plan.id] = plan.title ?? plan.name ?? "Unknown Plan";
1012
- });
1013
- return planTitles;
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
- getPricingById(pricingData) {
1016
- const pricing = {};
1017
- pricingData.plans?.forEach((plan) => {
1018
- plan.pricing?.forEach((p) => {
1019
- pricing[p.id] = p;
1020
- });
1021
- });
1022
- return pricing;
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/models/PurchaseInfo.ts
1028
- var PurchaseInfo = class {
1029
- email;
1030
- firstName;
1031
- lastName;
1032
- userId;
1033
- planId;
1034
- pricingId;
1035
- licenseId;
1036
- expiration;
1037
- canceled;
1038
- subscriptionId;
1039
- billingCycle;
1040
- quota;
1041
- initialAmount;
1042
- renewalAmount;
1043
- currency;
1044
- renewalDate;
1045
- paymentMethod;
1046
- created;
1047
- constructor(user, license, subscription) {
1048
- this.email = user.email;
1049
- this.firstName = user.first ?? "";
1050
- this.lastName = user.last ?? "";
1051
- this.userId = idToString(license.user_id);
1052
- this.canceled = license.is_cancelled ?? false;
1053
- this.expiration = license.expiration ? parseDateTime(license.expiration) : null;
1054
- this.licenseId = idToString(license.id);
1055
- this.planId = idToString(license.plan_id);
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
- isPlan(planId) {
1077
- return this.planId === idToString(planId);
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
- isFromPlans(planIds) {
1080
- return planIds.some((planId) => this.isPlan(planId));
1487
+ createProcessor(config) {
1488
+ return (request) => this.process(config, request);
1081
1489
  }
1082
- toData() {
1083
- return {
1084
- email: this.email,
1085
- firstName: this.firstName,
1086
- lastName: this.lastName,
1087
- userId: this.userId,
1088
- planId: this.planId,
1089
- pricingId: this.pricingId,
1090
- licenseId: this.licenseId,
1091
- expiration: this.expiration,
1092
- canceled: this.canceled,
1093
- subscriptionId: this.subscriptionId,
1094
- billingCycle: this.billingCycle,
1095
- quota: this.quota,
1096
- isActive: this.isActive,
1097
- initialAmount: this.initialAmount,
1098
- renewalAmount: this.renewalAmount,
1099
- currency: this.currency,
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
- get isActive() {
1106
- if (this.canceled) return false;
1107
- if (this.expiration && this.expiration < /* @__PURE__ */ new Date()) return false;
1108
- return true;
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
- hasSubscription() {
1111
- return this.subscriptionId !== null;
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
- isAnnual() {
1114
- return this.billingCycle === BILLING_CYCLE.YEARLY;
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
- isMonthly() {
1117
- return this.billingCycle === BILLING_CYCLE.MONTHLY;
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
- isOneOff() {
1120
- return this.billingCycle === BILLING_CYCLE.ONEOFF || this.billingCycle === null;
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
- getPlanTitle(pricingData) {
1123
- const plan = pricingData?.plans?.find((p) => isIdsEqual(p.id, this.planId));
1124
- return plan?.title ?? plan?.name ?? "Deleted Plan";
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/services/PurchaseService.ts
1130
- var PurchaseService = class {
1131
- constructor(api) {
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
- * Retrieve purchase information from the Freemius API based on the license ID.
1136
- *
1137
- * The license is the primary entitlement for a purchase, and it may or may not be associated with a subscription.
1138
- * With this method, you can retrieve detailed information about the purchase, including user details, plan, expiration, and more.
1139
- */
1140
- async retrievePurchase(licenseId) {
1141
- const [license, subscription] = await Promise.all([await this.api.license.retrieve(licenseId), await this.api.license.retrieveSubscription(licenseId)]);
1142
- if (!license) return null;
1143
- const user = await this.api.user.retrieve(license.user_id);
1144
- if (!user) return null;
1145
- return new PurchaseInfo(user, license, subscription);
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
- * A helper method to retrieve raw purchase data instead of a full PurchaseInfo object.
1149
- *
1150
- * This is useful when passing data from server to client in frameworks like Next.js, where only serializable data should be sent.
1151
- */
1152
- async retrievePurchaseData(licenseId) {
1153
- const purchaseInfo = await this.retrievePurchase(licenseId);
1154
- if (!purchaseInfo) return null;
1155
- return purchaseInfo.toData();
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
- * Retrieve a list of active subscriptions for a user. You can use this method to find or sync subscriptions from freemius to your system.
1159
- */
1160
- async retrieveSubscriptions(userId, pagination) {
1161
- const user = await this.api.user.retrieve(userId);
1162
- if (!user) return [];
1163
- const subscriptions = await this.api.user.retrieveSubscriptions(userId, { filter: "active" }, pagination);
1164
- if (!subscriptions || !subscriptions.length) return [];
1165
- const licenseSubscriptionPromises = subscriptions.map(async (subscription) => {
1166
- const license = await this.api.license.retrieve(subscription.license_id);
1167
- if (!license) return null;
1168
- return new PurchaseInfo(user, license, subscription);
1169
- });
1170
- return await Promise.all(licenseSubscriptionPromises).then((results) => results.filter((result) => result !== null).sort((a, b) => b.created.getTime() - a.created.getTime()));
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
- * Retrieve a list of purchase data for a user.
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 retrieveSubscriptionsData(userId, pagination) {
1178
- const purchaseInfos = await this.retrieveSubscriptions(userId, pagination);
1179
- return purchaseInfos.map((info) => info.toData());
1825
+ async retrieveData(option) {
1826
+ return this.repository.retrievePortalDataByUserId(option);
1180
1827
  }
1181
- async retrieveBySubscription(subscription, subscriptionUser) {
1182
- if (!subscription.license_id) return null;
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
- async retrieveActiveSubscriptionByEmail(email, pagination) {
1190
- const user = await this.api.user.retrieveByEmail(email);
1191
- if (!user) return null;
1192
- return await this.retrieveSubscriptions(user.id, pagination);
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(type, handler) {
1206
- if (!this.eventHandlers.has(type)) this.eventHandlers.set(type, /* @__PURE__ */ new Set());
1207
- const existingHandlers = this.eventHandlers.get(type);
1208
- existingHandlers?.add(handler);
1209
- return this;
1210
- }
1211
- off(type, handler) {
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
- onMultiple(handlers) {
1219
- for (const [type, handler] of Object.entries(handlers)) if (handler) this.on(type, handler);
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(type) {
1223
- this.eventHandlers.delete(type);
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
- constructor(productId, apiKey, secretKey, publicKey) {
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.checkout = new CheckoutService(productId, publicKey, secretKey);
2476
+ this.auth = new AuthService(productId, secretKey);
2477
+ this.pricing = new PricingService(this.api);
1422
2478
  this.purchase = new PurchaseService(this.api);
1423
- this.customerPortal = new CustomerPortalService(this.api, this.checkout);
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, PurchaseInfo, idToNumber, idToString, isIdsEqual, parseBillingCycle, parseCurrency, parseDate, parseDateTime, parseNumber, parsePaymentMethod };
2486
+ export { BILLING_CYCLE, CURRENCY, Freemius, idToNumber, idToString, isIdsEqual, parseBillingCycle, parseCurrency, parseDate, parseDateTime, parseNumber, parsePaymentMethod };
1430
2487
  //# sourceMappingURL=index.mjs.map