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