@freemius/sdk 0.0.1

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 ADDED
@@ -0,0 +1,1463 @@
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const crypto = __toESM(require("crypto"));
25
+ const openapi_fetch = __toESM(require("openapi-fetch"));
26
+ const __freemius_checkout = __toESM(require("@freemius/checkout"));
27
+
28
+ //#region src/contracts/types.ts
29
+ /**
30
+ * This file holds all generic types used across the SDK, not specific to any contract.
31
+ */
32
+ let BILLING_CYCLE = /* @__PURE__ */ function(BILLING_CYCLE$1) {
33
+ BILLING_CYCLE$1["MONTHLY"] = "monthly";
34
+ BILLING_CYCLE$1["YEARLY"] = "yearly";
35
+ BILLING_CYCLE$1["ONEOFF"] = "oneoff";
36
+ return BILLING_CYCLE$1;
37
+ }({});
38
+ let CURRENCY = /* @__PURE__ */ function(CURRENCY$1) {
39
+ CURRENCY$1["USD"] = "USD";
40
+ CURRENCY$1["EUR"] = "EUR";
41
+ CURRENCY$1["GBP"] = "GBP";
42
+ return CURRENCY$1;
43
+ }({});
44
+
45
+ //#endregion
46
+ //#region src/api/parser.ts
47
+ function idToNumber(id) {
48
+ if (typeof id === "number") return id;
49
+ else if (typeof id === "bigint") return Number(id);
50
+ else if (typeof id === "string") {
51
+ const parsed = Number.parseInt(id, 10);
52
+ if (Number.isNaN(parsed)) throw new Error(`Invalid FSId: ${id}`);
53
+ return parsed;
54
+ } else throw new Error(`Unsupported FSId type: ${typeof id}`);
55
+ }
56
+ function idToString(id) {
57
+ if (typeof id === "string") return id;
58
+ else if (typeof id === "number" || typeof id === "bigint") return String(id);
59
+ else throw new Error(`Unsupported FSId type: ${typeof id}`);
60
+ }
61
+ function isIdsEqual(id1, id2) {
62
+ return idToString(id1) === idToString(id2);
63
+ }
64
+ function parseBillingCycle(cycle) {
65
+ const billingCycle = Number.parseInt(cycle?.toString() ?? "", 10);
66
+ if (billingCycle === 1) return BILLING_CYCLE.MONTHLY;
67
+ if (billingCycle === 12) return BILLING_CYCLE.YEARLY;
68
+ return BILLING_CYCLE.ONEOFF;
69
+ }
70
+ function parseNumber(value) {
71
+ if (typeof value === "number") return value;
72
+ else if (typeof value === "string") {
73
+ const parsed = Number.parseFloat(value);
74
+ return Number.isNaN(parsed) ? null : parsed;
75
+ } else return null;
76
+ }
77
+ function parseDateTime(dateString) {
78
+ if (!dateString) return null;
79
+ const dateParts = dateString.split(" ");
80
+ if (dateParts.length !== 2) return null;
81
+ const date = dateParts[0].split("-");
82
+ const time = dateParts[1].split(":");
83
+ if (date.length !== 3 || time.length !== 3) return null;
84
+ const year = Number.parseInt(date[0]);
85
+ const month = Number.parseInt(date[1]) - 1;
86
+ const day = Number.parseInt(date[2]);
87
+ const hours = Number.parseInt(time[0]);
88
+ const minutes = Number.parseInt(time[1]);
89
+ const seconds = Number.parseInt(time[2]);
90
+ if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day) || Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds)) return null;
91
+ const utcDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
92
+ return utcDate;
93
+ }
94
+ function parseDate(dateString) {
95
+ if (!dateString) return null;
96
+ return parseDateTime(dateString + " 00:00:00");
97
+ }
98
+ function parseCurrency(currency) {
99
+ switch (currency?.toLowerCase?.()) {
100
+ case "usd": return CURRENCY.USD;
101
+ case "eur": return CURRENCY.EUR;
102
+ case "gbp": return CURRENCY.GBP;
103
+ default: return null;
104
+ }
105
+ }
106
+ function parsePaymentMethod(gateway) {
107
+ return gateway?.startsWith("stripe") ? "card" : gateway?.startsWith("paypal") ? "paypal" : null;
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/api/client.ts
112
+ function createApiClient(baseUrl, bearerToken) {
113
+ return (0, openapi_fetch.default)({
114
+ baseUrl,
115
+ headers: { Authorization: bearerToken ? `Bearer ${bearerToken}` : void 0 }
116
+ });
117
+ }
118
+
119
+ //#endregion
120
+ //#region src/api/ApiBase.ts
121
+ const PAGING_DEFAULT_LIMIT = 150;
122
+ const defaultPagingOptions = {
123
+ count: PAGING_DEFAULT_LIMIT,
124
+ offset: 0
125
+ };
126
+ var ApiBase = class {
127
+ productId;
128
+ constructor(productId, client) {
129
+ this.client = client;
130
+ this.productId = idToNumber(productId);
131
+ }
132
+ /**
133
+ * Async generator that yields all entities by paginating through retrieveMany.
134
+ * @param filter Optional filter for entities
135
+ * @param pageSize Optional page size (default: PAGING_DEFAULT_LIMIT)
136
+ *
137
+ * @example
138
+ * // Usage example:
139
+ * for await (const entity of apiInstance.iterateAll({ status: 'active' })) {
140
+ * console.log(entity);
141
+ * }
142
+ */
143
+ async *iterateAll(filter, pageSize = PAGING_DEFAULT_LIMIT) {
144
+ let offset = 0;
145
+ while (true) {
146
+ const page = await this.retrieveMany(filter, {
147
+ count: pageSize,
148
+ offset
149
+ });
150
+ if (!page.length) break;
151
+ for (const entity of page) yield entity;
152
+ if (page.length < pageSize) break;
153
+ offset += page.length;
154
+ }
155
+ }
156
+ getPagingParams(paging = defaultPagingOptions) {
157
+ return {
158
+ count: paging.count ?? PAGING_DEFAULT_LIMIT,
159
+ offset: paging.offset ?? 0
160
+ };
161
+ }
162
+ getIdForPath(id) {
163
+ return idToNumber(id);
164
+ }
165
+ isGoodResponse(response) {
166
+ return response.status >= 200 && response.status < 300;
167
+ }
168
+ };
169
+
170
+ //#endregion
171
+ //#region src/api/License.ts
172
+ var License = class extends ApiBase {
173
+ async retrieve(licenseId) {
174
+ const licenseResponse = await this.client.GET(`/products/{product_id}/licenses/{license_id}.json`, { params: { path: {
175
+ product_id: this.productId,
176
+ license_id: this.getIdForPath(licenseId)
177
+ } } });
178
+ if (!this.isGoodResponse(licenseResponse.response) || !licenseResponse.data || !licenseResponse.data.id) return null;
179
+ return licenseResponse.data;
180
+ }
181
+ async retrieveMany(filter, pagination) {
182
+ const response = await this.client.GET(`/products/{product_id}/licenses.json`, { params: {
183
+ path: { product_id: this.productId },
184
+ query: {
185
+ ...this.getPagingParams(pagination),
186
+ ...filter ?? {}
187
+ }
188
+ } });
189
+ if (!this.isGoodResponse(response.response) || !response.data || !Array.isArray(response.data.licenses)) return [];
190
+ return response.data.licenses;
191
+ }
192
+ async retrieveSubscription(licenseId) {
193
+ const subscriptionResponse = await this.client.GET(`/products/{product_id}/licenses/{license_id}/subscription.json`, { params: { path: {
194
+ product_id: this.productId,
195
+ license_id: this.getIdForPath(licenseId)
196
+ } } });
197
+ if (!this.isGoodResponse(subscriptionResponse.response) || !subscriptionResponse.data || !subscriptionResponse.data.id) return null;
198
+ return subscriptionResponse.data;
199
+ }
200
+ async retrieveCheckoutUpgradeAuthorization(licenseId) {
201
+ const response = await this.client.POST(`/products/{product_id}/licenses/{license_id}/checkout/link.json`, {
202
+ params: { path: {
203
+ product_id: this.productId,
204
+ license_id: this.getIdForPath(licenseId)
205
+ } },
206
+ body: { is_payment_method_update: true }
207
+ });
208
+ if (!this.isGoodResponse(response.response) || !response.data || !response.data.settings) return null;
209
+ return response.data.settings.authorization;
210
+ }
211
+ };
212
+
213
+ //#endregion
214
+ //#region src/api/Product.ts
215
+ var Product = class extends ApiBase {
216
+ async retrieve() {
217
+ 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;
219
+ return response.data;
220
+ }
221
+ async retrieveMany() {
222
+ throw new Error("retrieveMany is not supported for Product API");
223
+ }
224
+ async retrievePricingData() {
225
+ 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;
227
+ return response.data;
228
+ }
229
+ };
230
+
231
+ //#endregion
232
+ //#region src/api/Subscription.ts
233
+ var Subscription = class extends ApiBase {
234
+ async retrieve(subscriptionId) {
235
+ const response = await this.client.GET(`/products/{product_id}/subscriptions/{subscription_id}.json`, { params: { path: {
236
+ product_id: this.productId,
237
+ subscription_id: this.getIdForPath(subscriptionId)
238
+ } } });
239
+ if (response.response.status !== 200 || !response.data || !response.data.id) return null;
240
+ return response.data;
241
+ }
242
+ async retrieveMany(filter, pagination) {
243
+ const response = await this.client.GET(`/products/{product_id}/subscriptions.json`, { params: {
244
+ path: { product_id: this.productId },
245
+ query: {
246
+ ...this.getPagingParams(pagination),
247
+ ...filter ?? {}
248
+ }
249
+ } });
250
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
251
+ return response.data.subscriptions;
252
+ }
253
+ };
254
+
255
+ //#endregion
256
+ //#region src/api/User.ts
257
+ const USER_FIELDS = "email,first,last,picture,is_verified,id,created,updated,is_marketing_allowed";
258
+ var User = class extends ApiBase {
259
+ async retrieve(userId) {
260
+ const userResponse = await this.client.GET(`/products/{product_id}/users/{user_id}.json`, { params: {
261
+ path: {
262
+ product_id: this.productId,
263
+ user_id: this.getIdForPath(userId)
264
+ },
265
+ query: { fields: USER_FIELDS }
266
+ } });
267
+ if (userResponse.response.status !== 200 || !userResponse.data || !userResponse.data.id) return null;
268
+ return userResponse.data;
269
+ }
270
+ async retrieveMany(filter, pagination) {
271
+ const response = await this.client.GET(`/products/{product_id}/users.json`, { params: {
272
+ path: { product_id: this.productId },
273
+ query: {
274
+ ...this.getPagingParams(pagination),
275
+ ...filter ?? {},
276
+ fields: USER_FIELDS
277
+ }
278
+ } });
279
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.users)) return [];
280
+ return response.data.users;
281
+ }
282
+ async retrieveByEmail(email) {
283
+ const response = await this.client.GET(`/products/{product_id}/users.json`, { params: {
284
+ path: { product_id: this.productId },
285
+ query: { email }
286
+ } });
287
+ if (!this.isGoodResponse(response.response) || !Array.isArray(response.data?.users)) return null;
288
+ return response.data.users?.[0] ?? null;
289
+ }
290
+ async retrieveBilling(userId) {
291
+ const billingResponse = await this.client.GET(`/products/{product_id}/users/{user_id}/billing.json`, { params: { path: {
292
+ product_id: this.productId,
293
+ user_id: this.getIdForPath(userId)
294
+ } } });
295
+ if (billingResponse.response.status !== 200 || !billingResponse.data || !billingResponse.data) return null;
296
+ return billingResponse.data;
297
+ }
298
+ async retrieveSubscriptions(userId, filters, pagination) {
299
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/subscriptions.json`, { params: {
300
+ path: {
301
+ product_id: this.productId,
302
+ user_id: this.getIdForPath(userId)
303
+ },
304
+ query: {
305
+ ...filters ?? {},
306
+ ...this.getPagingParams(pagination)
307
+ }
308
+ } });
309
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
310
+ return response.data.subscriptions;
311
+ }
312
+ async retrieveLicenses(userId, filters, pagination) {
313
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/licenses.json`, { params: {
314
+ path: {
315
+ product_id: this.productId,
316
+ user_id: this.getIdForPath(userId)
317
+ },
318
+ query: {
319
+ ...filters ?? {},
320
+ ...this.getPagingParams(pagination)
321
+ }
322
+ } });
323
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.licenses)) return [];
324
+ return response.data.licenses;
325
+ }
326
+ async retrievePayments(userId, filters, pagination) {
327
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/payments.json`, { params: {
328
+ path: {
329
+ product_id: this.productId,
330
+ user_id: this.getIdForPath(userId)
331
+ },
332
+ query: {
333
+ ...filters ?? {},
334
+ ...this.getPagingParams(pagination)
335
+ }
336
+ } });
337
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.payments)) return [];
338
+ return response.data.payments;
339
+ }
340
+ };
341
+
342
+ //#endregion
343
+ //#region src/services/ApiService.ts
344
+ const API_ENDPOINT_PRODUCTION = "https://api.freemius.com/v1/";
345
+ const API_ENDPOINT_TEST = "http://api.freemius-local.com:8080/v1/";
346
+ var ApiService = class {
347
+ client;
348
+ productId;
349
+ user;
350
+ license;
351
+ product;
352
+ subscription;
353
+ baseUrl;
354
+ constructor(productId, apiKey, secretKey, publicKey) {
355
+ this.secretKey = secretKey;
356
+ this.publicKey = publicKey;
357
+ const isTestServer = process.env.FS__INTERNAL__IS_DEVELOPMENT_MODE === "true";
358
+ this.baseUrl = isTestServer ? API_ENDPOINT_TEST : API_ENDPOINT_PRODUCTION;
359
+ this.client = createApiClient(this.baseUrl, apiKey);
360
+ this.productId = idToString(productId);
361
+ this.user = new User(this.productId, this.client);
362
+ this.license = new License(this.productId, this.client);
363
+ this.product = new Product(this.productId, this.client);
364
+ this.subscription = new Subscription(this.productId, this.client);
365
+ }
366
+ /**
367
+ * Low level API client for direct access to the Freemius API.
368
+ * Use this for advanced use cases where you need to make custom API calls.
369
+ *
370
+ * For regular operations, prefer using the provided services like `User`, `Subscription`, `License` etc.
371
+ */
372
+ get __unstable_ApiClient() {
373
+ return this.client;
374
+ }
375
+ createUrl(path) {
376
+ path = path.replace(/^\/+/, "");
377
+ return `${this.baseUrl}products/${this.productId}/${path}`;
378
+ }
379
+ /**
380
+ * Generate signed URL for the given full URL
381
+ */
382
+ getSignedUrl(fullUrl) {
383
+ const url = new URL(fullUrl);
384
+ const resourcePath = url.pathname;
385
+ const auth = this.generateAuthorizationParams(resourcePath);
386
+ url.searchParams.set("auth_date", auth.date);
387
+ url.searchParams.set("authorization", auth.authorization);
388
+ return url.toString();
389
+ }
390
+ /**
391
+ * Generate authorization parameters for signing
392
+ */
393
+ generateAuthorizationParams(resourcePath, method = "GET", jsonEncodedParams = "", contentType = "") {
394
+ const eol = "\n";
395
+ let contentMd5 = "";
396
+ const date = this.toDateTimeString(/* @__PURE__ */ new Date());
397
+ if (["POST", "PUT"].includes(method) && jsonEncodedParams) contentMd5 = crypto.createHash("md5").update(jsonEncodedParams).digest("hex");
398
+ const stringToSign = [
399
+ method,
400
+ contentMd5,
401
+ contentType,
402
+ date,
403
+ resourcePath
404
+ ].join(eol);
405
+ const authType = this.secretKey !== this.publicKey ? "FS" : "FSP";
406
+ const signature = crypto.createHmac("sha256", this.secretKey).update(stringToSign).digest("hex");
407
+ const base64 = this.base64UrlEncode(signature);
408
+ return {
409
+ date,
410
+ authorization: `${authType} ${this.productId}:${this.publicKey}:${base64}`
411
+ };
412
+ }
413
+ /**
414
+ * Base64 encoding that doesn't need to be urlencode()ed.
415
+ * Exactly the same as base64_encode except it uses
416
+ * - instead of +
417
+ * _ instead of /
418
+ */
419
+ base64UrlEncode(input) {
420
+ return Buffer.from(input, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
421
+ }
422
+ toDateTimeString(date) {
423
+ const year = date.getUTCFullYear();
424
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
425
+ const day = String(date.getUTCDate()).padStart(2, "0");
426
+ const hours = String(date.getUTCHours()).padStart(2, "0");
427
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
428
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
429
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
430
+ }
431
+ };
432
+
433
+ //#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
445
+ /**
446
+ * A builder class for constructing checkout parameters. This class provides a fluent
447
+ * API to create Checkout parameters for a product with various configurations.
448
+ *
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.
454
+ */
455
+ var CheckoutBuilder = class CheckoutBuilder {
456
+ constructor(options, productId, publicKey, secretKey) {
457
+ this.options = options;
458
+ this.productId = productId;
459
+ this.publicKey = publicKey;
460
+ this.secretKey = secretKey;
461
+ }
462
+ /**
463
+ * Enables sandbox mode for testing purposes.
464
+ *
465
+ * @returns A new builder instance with sandbox configuration
466
+ */
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({
471
+ ...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);
477
+ }
478
+ /**
479
+ * Sets user information for the checkout session.
480
+ *
481
+ * @param user User object with email and optional name fields. The shape matches the session from `better-auth` or next-auth packages. Also handles `null` or `undefined` gracefully.
482
+ * @param readonly If true, the user information will be read-only in the checkout session.
483
+ *
484
+ * @returns A new builder instance with user configuration
485
+ */
486
+ withUser(user, readonly = true) {
487
+ if (!user) return this;
488
+ let firstName = user.firstName ?? "";
489
+ let lastName = user.lastName ?? "";
490
+ if (user.name) {
491
+ const { firstName: fn, lastName: ln } = splitName(user.name);
492
+ firstName = fn;
493
+ lastName = ln;
494
+ }
495
+ return new CheckoutBuilder({
496
+ ...this.options,
497
+ user_email: user.email,
498
+ user_first_name: firstName,
499
+ user_last_name: lastName,
500
+ readonly_user: readonly
501
+ }, this.productId, this.publicKey, this.secretKey);
502
+ }
503
+ /**
504
+ * Applies recommended UI settings for better user experience.
505
+ * This includes fullscreen mode, upsells, refund badge, and reviews display.
506
+ *
507
+ * @returns A new builder instance with recommended UI settings
508
+ */
509
+ withRecommendation() {
510
+ return new CheckoutBuilder({
511
+ ...this.options,
512
+ fullscreen: true,
513
+ show_refund_badge: true,
514
+ show_reviews: true,
515
+ locale: "auto",
516
+ currency: "auto"
517
+ }, this.productId, this.publicKey, this.secretKey);
518
+ }
519
+ /**
520
+ * Sets the plan ID for the checkout.
521
+ *
522
+ * @param planId The plan ID to purchase
523
+ * @returns A new builder instance with plan ID set
524
+ */
525
+ withPlan(planId) {
526
+ return new CheckoutBuilder({
527
+ ...this.options,
528
+ plan_id: planId.toString()
529
+ }, this.productId, this.publicKey, this.secretKey);
530
+ }
531
+ /**
532
+ * Sets the number of licenses to purchase.
533
+ *
534
+ * @param count Number of licenses
535
+ * @returns A new builder instance with license count set
536
+ */
537
+ withQuota(count) {
538
+ return new CheckoutBuilder({
539
+ ...this.options,
540
+ licenses: count
541
+ }, this.productId, this.publicKey, this.secretKey);
542
+ }
543
+ withPricing(pricingId) {
544
+ return new CheckoutBuilder({
545
+ ...this.options,
546
+ pricing_id: pricingId.toString()
547
+ }, this.productId, this.publicKey, this.secretKey);
548
+ }
549
+ withTitle(title) {
550
+ return new CheckoutBuilder({
551
+ ...this.options,
552
+ title
553
+ }, this.productId, this.publicKey, this.secretKey);
554
+ }
555
+ /**
556
+ * Sets a coupon code for the checkout.
557
+ *
558
+ * @param coupon The coupon code to apply
559
+ * @param hideUI Whether to hide the coupon input field from users
560
+ * @returns A new builder instance with coupon configuration
561
+ */
562
+ withCoupon(options) {
563
+ const { code: coupon, hideUI = false } = options;
564
+ const newOptions = {
565
+ ...this.options,
566
+ hide_coupon: hideUI
567
+ };
568
+ if (coupon !== void 0) newOptions.coupon = coupon;
569
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
570
+ }
571
+ /**
572
+ * Enables trial mode for the checkout.
573
+ *
574
+ * @param mode Trial type - true/false for plan default, or specific 'free'/'paid' mode
575
+ * @returns A new builder instance with trial configuration
576
+ */
577
+ inTrial(mode = true) {
578
+ return new CheckoutBuilder({
579
+ ...this.options,
580
+ trial: mode
581
+ }, this.productId, this.publicKey, this.secretKey);
582
+ }
583
+ /**
584
+ * Configures the visual layout and appearance of the checkout.
585
+ *
586
+ * @param options Appearance configuration options
587
+ * @returns A new builder instance with appearance configuration
588
+ */
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);
597
+ }
598
+ /**
599
+ * Configures discount display settings.
600
+ *
601
+ * @param options Discount configuration options
602
+ * @returns A new builder instance with discount configuration
603
+ */
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);
611
+ }
612
+ /**
613
+ * Configures billing cycle selector interface.
614
+ *
615
+ * @param selector Type of billing cycle selector to show
616
+ * @param defaultCycle Default billing cycle to select
617
+ * @returns A new builder instance with billing cycle configuration
618
+ */
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);
624
+ }
625
+ /**
626
+ * Sets the language/locale for the checkout.
627
+ *
628
+ * @param locale Language setting - 'auto', 'auto-beta', or specific locale like 'en_US'
629
+ * @returns A new builder instance with locale configuration
630
+ */
631
+ withLanguage(locale = "auto") {
632
+ return new CheckoutBuilder({
633
+ ...this.options,
634
+ language: locale,
635
+ locale
636
+ }, this.productId, this.publicKey, this.secretKey);
637
+ }
638
+ /**
639
+ * Configures review and badge display settings.
640
+ *
641
+ * @param options Review and badge configuration
642
+ * @returns A new builder instance with reviews and badges configuration
643
+ */
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);
651
+ }
652
+ /**
653
+ * Enhanced currency configuration.
654
+ *
655
+ * @param currency Primary currency or 'auto' for automatic detection
656
+ * @param defaultCurrency Default currency when using 'auto'
657
+ * @param showInlineSelector Whether to show inline currency selector
658
+ * @returns A new builder instance with currency configuration
659
+ */
660
+ withCurrency(currency, defaultCurrency = "usd", showInlineSelector = true) {
661
+ const options = {
662
+ ...this.options,
663
+ show_inline_currency_selector: showInlineSelector,
664
+ default_currency: defaultCurrency
665
+ };
666
+ if (currency !== "auto") options.currency = currency;
667
+ return new CheckoutBuilder(options, this.productId, this.publicKey, this.secretKey);
668
+ }
669
+ /**
670
+ * Configures navigation and cancel behavior.
671
+ *
672
+ * @param cancelUrl URL for back button when in page mode
673
+ * @param cancelIcon Custom cancel icon URL
674
+ * @returns A new builder instance with navigation configuration
675
+ */
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);
681
+ }
682
+ /**
683
+ * Associates purchases with an affiliate account.
684
+ *
685
+ * @param userId Affiliate user ID
686
+ * @returns A new builder instance with affiliate configuration
687
+ */
688
+ withAffiliate(userId) {
689
+ return new CheckoutBuilder({
690
+ ...this.options,
691
+ affiliate_user_id: userId
692
+ }, this.productId, this.publicKey, this.secretKey);
693
+ }
694
+ /**
695
+ * Sets a custom image/icon for the checkout.
696
+ *
697
+ * @param imageUrl Secure HTTPS URL to the image
698
+ * @returns A new builder instance with custom image
699
+ */
700
+ withImage(imageUrl) {
701
+ return new CheckoutBuilder({
702
+ ...this.options,
703
+ image: imageUrl
704
+ }, this.productId, this.publicKey, this.secretKey);
705
+ }
706
+ /**
707
+ * Configures the checkout for license renewal.
708
+ *
709
+ * @param licenseKey The license key to renew
710
+ * @returns A new builder instance configured for renewal
711
+ */
712
+ forRenewal(licenseKey) {
713
+ return new CheckoutBuilder({
714
+ ...this.options,
715
+ license_key: licenseKey
716
+ }, this.productId, this.publicKey, this.secretKey);
717
+ }
718
+ /**
719
+ * Builds and returns the final checkout options to be used with the `@freemius/checkout` package.
720
+ *
721
+ * @returns The constructed CheckoutOptions object
722
+ */
723
+ toOptions(additionalOptions) {
724
+ return {
725
+ ...this.options,
726
+ ...additionalOptions,
727
+ product_id: this.productId
728
+ };
729
+ }
730
+ toLink() {
731
+ const checkoutOptions = (0, __freemius_checkout.convertCheckoutOptionsToQueryParams)(this.options);
732
+ const queryParams = (0, __freemius_checkout.buildFreemiusQueryFromOptions)(checkoutOptions);
733
+ const url = new URL(`https://checkout.freemius.com/product/${this.productId}/`);
734
+ url.search = queryParams;
735
+ return url.href;
736
+ }
737
+ };
738
+
739
+ //#endregion
740
+ //#region src/models/CheckoutRedirectInfo.ts
741
+ var CheckoutRedirectInfo = class {
742
+ user_id;
743
+ plan_id;
744
+ email;
745
+ pricing_id;
746
+ currency;
747
+ license_id;
748
+ expiration;
749
+ quota;
750
+ action;
751
+ amount;
752
+ tax;
753
+ type;
754
+ subscription_id;
755
+ billing_cycle;
756
+ payment_id;
757
+ constructor(data) {
758
+ this.user_id = idToString(data.user_id);
759
+ this.plan_id = idToString(data.plan_id);
760
+ this.email = data.email;
761
+ this.pricing_id = idToString(data.pricing_id);
762
+ this.currency = data.currency ? parseCurrency(data.currency) : CURRENCY.USD;
763
+ this.license_id = idToString(data.license_id);
764
+ this.expiration = data.expiration ? parseDateTime(data.expiration) : null;
765
+ this.quota = data.quota ? parseNumber(data.quota) : null;
766
+ this.action = data.action ? data.action : null;
767
+ this.amount = parseNumber(data.amount);
768
+ this.tax = parseNumber(data.tax);
769
+ this.type = data.type === "subscription" ? "subscription" : "one-off";
770
+ this.subscription_id = data.subscription_id ? idToString(data.subscription_id) : null;
771
+ this.billing_cycle = data.billing_cycle ? parseBillingCycle(data.billing_cycle) : null;
772
+ this.payment_id = data.payment_id ? idToString(data.payment_id) : null;
773
+ }
774
+ isSubscription() {
775
+ return this.type === "subscription";
776
+ }
777
+ toData() {
778
+ return {
779
+ user_id: this.user_id,
780
+ plan_id: this.plan_id,
781
+ email: this.email,
782
+ pricing_id: this.pricing_id,
783
+ currency: this.currency,
784
+ license_id: this.license_id,
785
+ expiration: this.expiration,
786
+ quota: this.quota,
787
+ action: this.action,
788
+ amount: this.amount,
789
+ tax: this.tax,
790
+ type: this.type,
791
+ subscription_id: this.subscription_id,
792
+ billing_cycle: this.billing_cycle,
793
+ payment_id: this.payment_id
794
+ };
795
+ }
796
+ };
797
+
798
+ //#endregion
799
+ //#region src/services/CheckoutService.ts
800
+ var CheckoutService = class {
801
+ constructor(productId, publicKey, secretKey) {
802
+ this.productId = productId;
803
+ this.publicKey = publicKey;
804
+ this.secretKey = secretKey;
805
+ }
806
+ /**
807
+ * Use this to build a Checkout for your product.
808
+ * You can build a Checkout link or options for the popup.
809
+ *
810
+ * @param withRecommendation If true, the checkout will include a recommendation for the user.
811
+ *
812
+ * @return A new instance of CheckoutBuilder with the product ID and public key.
813
+ *
814
+ * @example
815
+ * Basic usage:
816
+ * ```typescript
817
+ * const options = freemius.checkout.params()
818
+ * .withUser(session?.user)
819
+ * .inSandbox()
820
+ * .withRecommendation()
821
+ * .toOptions(); // Or .toLink() for a hosted checkout link
822
+ * ```
823
+ *
824
+ * @example
825
+ * Advanced configuration:
826
+ * ```typescript
827
+ * const checkoutOptions = freemius.checkout.params()
828
+ * .withUser(user, true)
829
+ * .withPlan('1234')
830
+ * .withQuota(5)
831
+ * .withCurrency('eur')
832
+ * .withCoupon({
833
+ * code: 'DISCOUNT2023',
834
+ * hideUI: false
835
+ * })
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();
859
+ * ```
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
+ *
879
+ * Useful for generating recommended checkout links for SaaS.
880
+ */
881
+ createUserLink(user, isSandbox = false) {
882
+ let builder = this.create().withUser(user);
883
+ if (isSandbox) builder = builder.inSandbox();
884
+ return builder.toLink();
885
+ }
886
+ /**
887
+ * Retrieves the sandbox parameters for the checkout.
888
+ *
889
+ * This shouldn't be used in production, but is useful for testing purposes.
890
+ *
891
+ * @note This is intentionally set as `async` because we would use the API in the future to generate more fine grained sandbox params (for example for a specific email address only).
892
+ *
893
+ * @todo - This has a duplication with the `inSandbox` method in the builder. Consider refactoring to avoid this duplication.
894
+ * Also think about whether we should make the builder's `inSandbox` method async as well.
895
+ */
896
+ 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
+ };
903
+ }
904
+ /**
905
+ * Processes the redirect from Freemius Checkout.
906
+ *
907
+ * This method verifies the signature in the URL and returns a CheckoutRedirectInfo object if successful.
908
+ *
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.
910
+ *
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/...`.
913
+ *
914
+ * @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
+ * );
922
+ * }
923
+ * ```
924
+ */
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);
944
+ }
945
+ };
946
+
947
+ //#endregion
948
+ //#region src/services/CustomerPortalService.ts
949
+ var CustomerPortalService = class {
950
+ constructor(api, checkout) {
951
+ this.api = api;
952
+ this.checkout = checkout;
953
+ }
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
+ }));
979
+ const checkoutOptions = { product_id: this.api.productId };
980
+ if (sandbox) checkoutOptions.sandbox = await this.checkout.getSandboxParams();
981
+ const billingData = {
982
+ user,
983
+ checkoutOptions,
984
+ billing,
985
+ subscriptions: {
986
+ primary: null,
987
+ active: [],
988
+ past: []
989
+ },
990
+ payments: portalPayments,
991
+ plans: pricingData.plans ?? [],
992
+ sellingUnit: pricingData.selling_unit_label ?? {
993
+ singular: "Unit",
994
+ plural: "Units"
995
+ },
996
+ productId: this.api.productId
997
+ };
998
+ subscriptions.forEach((subscription) => {
999
+ const isActive = null === subscription.canceled_at;
1000
+ const subscriptionData = {
1001
+ subscriptionId: idToString(subscription.id),
1002
+ planId: idToString(subscription.plan_id),
1003
+ pricingId: idToString(subscription.pricing_id),
1004
+ planTitle: planTitles[subscription.plan_id] ?? `Plan ${subscription.plan_id}`,
1005
+ renewalAmount: parseNumber(subscription.renewal_amount),
1006
+ initialAmount: parseNumber(subscription.initial_amount),
1007
+ billingCycle: parseBillingCycle(subscription.billing_cycle),
1008
+ isActive,
1009
+ renewalDate: parseDateTime(subscription.next_payment),
1010
+ licenseId: idToString(subscription.license_id),
1011
+ currency: parseCurrency(subscription.currency) ?? CURRENCY.USD,
1012
+ createdAt: parseDateTime(subscription.created) ?? /* @__PURE__ */ new Date(),
1013
+ cancelledAt: subscription.canceled_at ? parseDateTime(subscription.canceled_at) : null,
1014
+ quota: allPricingsById[subscription.pricing_id]?.licenses ?? null,
1015
+ paymentMethod: parsePaymentMethod(subscription.gateway)
1016
+ };
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;
1020
+ });
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;
1026
+ }
1027
+ /**
1028
+ * @todo - Implement this method to handle actions like get cancel coupon, cancel subscription, update billing, get upgrade auth for Checkout etc.
1029
+ */
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;
1036
+ }
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;
1045
+ }
1046
+ };
1047
+
1048
+ //#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);
1096
+ }
1097
+ }
1098
+ isPlan(planId) {
1099
+ return this.planId === idToString(planId);
1100
+ }
1101
+ isFromPlans(planIds) {
1102
+ return planIds.some((planId) => this.isPlan(planId));
1103
+ }
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
+ };
1126
+ }
1127
+ get isActive() {
1128
+ if (this.canceled) return false;
1129
+ if (this.expiration && this.expiration < /* @__PURE__ */ new Date()) return false;
1130
+ return true;
1131
+ }
1132
+ hasSubscription() {
1133
+ return this.subscriptionId !== null;
1134
+ }
1135
+ isAnnual() {
1136
+ return this.billingCycle === BILLING_CYCLE.YEARLY;
1137
+ }
1138
+ isMonthly() {
1139
+ return this.billingCycle === BILLING_CYCLE.MONTHLY;
1140
+ }
1141
+ isOneOff() {
1142
+ return this.billingCycle === BILLING_CYCLE.ONEOFF || this.billingCycle === null;
1143
+ }
1144
+ getPlanTitle(pricingData) {
1145
+ const plan = pricingData?.plans?.find((p) => isIdsEqual(p.id, this.planId));
1146
+ return plan?.title ?? plan?.name ?? "Deleted Plan";
1147
+ }
1148
+ };
1149
+
1150
+ //#endregion
1151
+ //#region src/services/PurchaseService.ts
1152
+ var PurchaseService = class {
1153
+ constructor(api) {
1154
+ this.api = api;
1155
+ }
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);
1168
+ }
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();
1178
+ }
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()));
1193
+ }
1194
+ /**
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.
1198
+ */
1199
+ async retrieveSubscriptionsData(userId, pagination) {
1200
+ const purchaseInfos = await this.retrieveSubscriptions(userId, pagination);
1201
+ return purchaseInfos.map((info) => info.toData());
1202
+ }
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);
1210
+ }
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);
1215
+ }
1216
+ };
1217
+
1218
+ //#endregion
1219
+ //#region src/webhook/WebhookListener.ts
1220
+ const SIGNATURE_HEADER = "x-signature";
1221
+ var WebhookListener = class {
1222
+ eventHandlers = /* @__PURE__ */ new Map();
1223
+ constructor(secretKey, onError = console.error) {
1224
+ this.secretKey = secretKey;
1225
+ this.onError = onError;
1226
+ }
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);
1238
+ return this;
1239
+ }
1240
+ onMultiple(handlers) {
1241
+ for (const [type, handler] of Object.entries(handlers)) if (handler) this.on(type, handler);
1242
+ return this;
1243
+ }
1244
+ removeAll(type) {
1245
+ this.eventHandlers.delete(type);
1246
+ return this;
1247
+ }
1248
+ getHandlerCount(type) {
1249
+ return this.eventHandlers.get(type)?.size ?? 0;
1250
+ }
1251
+ getEventTypeCount() {
1252
+ return this.eventHandlers.size;
1253
+ }
1254
+ getRegisteredEventTypes() {
1255
+ return Array.from(this.eventHandlers.keys());
1256
+ }
1257
+ hasHandlers(type) {
1258
+ const handlers = this.eventHandlers.get(type);
1259
+ return handlers !== void 0 && handlers.size > 0;
1260
+ }
1261
+ hasHandler(type, handler) {
1262
+ const handlers = this.eventHandlers.get(type);
1263
+ return handlers ? handlers.has(handler) : false;
1264
+ }
1265
+ getHandlers(type) {
1266
+ return this.eventHandlers.get(type) || /* @__PURE__ */ new Set();
1267
+ }
1268
+ getTotalHandlerCount() {
1269
+ let total = 0;
1270
+ for (const handlers of this.eventHandlers.values()) total += handlers.size;
1271
+ return total;
1272
+ }
1273
+ /**
1274
+ * Verify hex HMAC signature against the raw body.
1275
+ */
1276
+ verifySignature(rawBody, signature) {
1277
+ if (!signature) return false;
1278
+ const mac = crypto.default.createHmac("sha256", this.secretKey).update(rawBody).digest("hex");
1279
+ try {
1280
+ return crypto.default.timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(signature, "hex"));
1281
+ } catch {
1282
+ return false;
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Process a normalized request.
1287
+ * Returns an object you can map to your framework's response easily.
1288
+ */
1289
+ async process(input) {
1290
+ const sig = this.getHeader(SIGNATURE_HEADER, input.headers);
1291
+ if (!this.verifySignature(input.rawBody, sig)) return {
1292
+ status: 401,
1293
+ success: false,
1294
+ error: "Invalid signature"
1295
+ };
1296
+ let evt;
1297
+ try {
1298
+ const parsed = JSON.parse(typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8"));
1299
+ if (!parsed || typeof parsed.type !== "string") return {
1300
+ status: 400,
1301
+ success: false,
1302
+ error: "Invalid payload"
1303
+ };
1304
+ evt = parsed;
1305
+ } catch {
1306
+ return {
1307
+ status: 400,
1308
+ success: false,
1309
+ error: "Malformed JSON"
1310
+ };
1311
+ }
1312
+ const eventType = evt.type;
1313
+ const eventHandlers = this.eventHandlers.get(eventType);
1314
+ if (!eventHandlers || eventHandlers.size === 0) console.warn(`No handlers registered for event type: ${eventType}`);
1315
+ try {
1316
+ const promises = Array.from(eventHandlers || []).map((handler) => {
1317
+ const typedHandler = handler;
1318
+ const typedEvent = evt;
1319
+ return typedHandler(typedEvent);
1320
+ });
1321
+ await Promise.all(promises);
1322
+ } catch (error) {
1323
+ this.onError?.(error);
1324
+ return {
1325
+ status: 500,
1326
+ success: false,
1327
+ error: "Internal Server Error"
1328
+ };
1329
+ }
1330
+ return {
1331
+ status: 200,
1332
+ success: true
1333
+ };
1334
+ }
1335
+ getHeader(name, headers) {
1336
+ const lname = name.toLowerCase();
1337
+ if (headers instanceof Headers) return headers.get(lname);
1338
+ const v = headers[lname] ?? headers[name];
1339
+ if (Array.isArray(v)) return v[0] ?? null;
1340
+ return v ?? null;
1341
+ }
1342
+ };
1343
+
1344
+ //#endregion
1345
+ //#region src/services/WebhookService.ts
1346
+ var WebhookService = class {
1347
+ constructor(secretKey) {
1348
+ this.secretKey = secretKey;
1349
+ }
1350
+ createListener(onError) {
1351
+ return new WebhookListener(this.secretKey, onError);
1352
+ }
1353
+ /**
1354
+ * WHATWG Fetch API adapter for modern JavaScript environments.
1355
+ *
1356
+ * Compatible with:
1357
+ * - Next.js App Router (route.ts files)
1358
+ * - Cloudflare Workers
1359
+ * - Deno
1360
+ * - Bun
1361
+ * - Vercel Edge Functions
1362
+ * - Any environment supporting the WHATWG Fetch API
1363
+ *
1364
+ * This method reads the request body as text and processes the webhook,
1365
+ * returning a standard Response object that can be directly returned
1366
+ * from your endpoint handler.
1367
+ *
1368
+ * @param listener - The webhook listener instance
1369
+ * @param request - The incoming Request object (WHATWG Fetch API)
1370
+ * @returns A Response object with the webhook processing result
1371
+ *
1372
+ * @example
1373
+ * ```typescript
1374
+ * // Next.js App Router (app/webhook/route.ts)
1375
+ * export async function POST(request: Request) {
1376
+ * const listener = webhookService.createListener();
1377
+ * return await webhookService.processFetch(listener, request);
1378
+ * }
1379
+ *
1380
+ * // Cloudflare Workers
1381
+ * export default {
1382
+ * async fetch(request: Request): Promise<Response> {
1383
+ * if (new URL(request.url).pathname === '/webhook') {
1384
+ * const listener = webhookService.createListener();
1385
+ * return await webhookService.processFetch(listener, request);
1386
+ * }
1387
+ * return new Response('Not Found', { status: 404 });
1388
+ * }
1389
+ * };
1390
+ * ```
1391
+ */
1392
+ async processFetch(listener, request) {
1393
+ const rawBody = await request.text();
1394
+ const result = await listener.process({
1395
+ headers: request.headers,
1396
+ rawBody
1397
+ });
1398
+ return new Response(JSON.stringify(result), {
1399
+ status: result.status,
1400
+ headers: { "Content-Type": "application/json" }
1401
+ });
1402
+ }
1403
+ /**
1404
+ * Native Node.js HTTP server adapter.
1405
+ *
1406
+ * Reads the raw body from the request stream and writes the HTTP response directly.
1407
+ *
1408
+ * @example
1409
+ * ```typescript
1410
+ * import { createServer } from 'http';
1411
+ *
1412
+ * const server = createServer(async (req, res) => {
1413
+ * if (req.url === '/webhook') {
1414
+ * await freemius.webhook.processNodeHttp(listener, req, res);
1415
+ * }
1416
+ * });
1417
+ * ```
1418
+ */
1419
+ async processNodeHttp(listener, req, res) {
1420
+ const chunks = [];
1421
+ for await (const chunk of req) chunks.push(chunk);
1422
+ const rawBody = Buffer.concat(chunks);
1423
+ const result = await listener.process({
1424
+ headers: req.headers,
1425
+ rawBody
1426
+ });
1427
+ res.statusCode = result.status;
1428
+ res.setHeader("Content-Type", "application/json");
1429
+ res.end(JSON.stringify(result));
1430
+ }
1431
+ };
1432
+
1433
+ //#endregion
1434
+ //#region src/Freemius.ts
1435
+ var Freemius = class {
1436
+ api;
1437
+ checkout;
1438
+ purchase;
1439
+ customerPortal;
1440
+ webhook;
1441
+ constructor(productId, apiKey, secretKey, publicKey) {
1442
+ this.api = new ApiService(productId, apiKey, secretKey, publicKey);
1443
+ this.checkout = new CheckoutService(productId, publicKey, secretKey);
1444
+ this.purchase = new PurchaseService(this.api);
1445
+ this.customerPortal = new CustomerPortalService(this.api, this.checkout);
1446
+ this.webhook = new WebhookService(secretKey);
1447
+ }
1448
+ };
1449
+
1450
+ //#endregion
1451
+ exports.BILLING_CYCLE = BILLING_CYCLE;
1452
+ exports.CURRENCY = CURRENCY;
1453
+ exports.Freemius = Freemius;
1454
+ exports.PurchaseInfo = PurchaseInfo;
1455
+ exports.idToNumber = idToNumber;
1456
+ exports.idToString = idToString;
1457
+ exports.isIdsEqual = isIdsEqual;
1458
+ exports.parseBillingCycle = parseBillingCycle;
1459
+ exports.parseCurrency = parseCurrency;
1460
+ exports.parseDate = parseDate;
1461
+ exports.parseDateTime = parseDateTime;
1462
+ exports.parseNumber = parseNumber;
1463
+ exports.parsePaymentMethod = parsePaymentMethod;