@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.mjs ADDED
@@ -0,0 +1,1430 @@
1
+ import * as crypto$1 from "crypto";
2
+ import crypto, { createHash, createHmac, timingSafeEqual } from "crypto";
3
+ import createClient from "openapi-fetch";
4
+ import { buildFreemiusQueryFromOptions, convertCheckoutOptionsToQueryParams } from "@freemius/checkout";
5
+
6
+ //#region src/contracts/types.ts
7
+ /**
8
+ * This file holds all generic types used across the SDK, not specific to any contract.
9
+ */
10
+ let BILLING_CYCLE = /* @__PURE__ */ function(BILLING_CYCLE$1) {
11
+ BILLING_CYCLE$1["MONTHLY"] = "monthly";
12
+ BILLING_CYCLE$1["YEARLY"] = "yearly";
13
+ BILLING_CYCLE$1["ONEOFF"] = "oneoff";
14
+ return BILLING_CYCLE$1;
15
+ }({});
16
+ let CURRENCY = /* @__PURE__ */ function(CURRENCY$1) {
17
+ CURRENCY$1["USD"] = "USD";
18
+ CURRENCY$1["EUR"] = "EUR";
19
+ CURRENCY$1["GBP"] = "GBP";
20
+ return CURRENCY$1;
21
+ }({});
22
+
23
+ //#endregion
24
+ //#region src/api/parser.ts
25
+ function idToNumber(id) {
26
+ if (typeof id === "number") return id;
27
+ else if (typeof id === "bigint") return Number(id);
28
+ else if (typeof id === "string") {
29
+ const parsed = Number.parseInt(id, 10);
30
+ if (Number.isNaN(parsed)) throw new Error(`Invalid FSId: ${id}`);
31
+ return parsed;
32
+ } else throw new Error(`Unsupported FSId type: ${typeof id}`);
33
+ }
34
+ function idToString(id) {
35
+ if (typeof id === "string") return id;
36
+ else if (typeof id === "number" || typeof id === "bigint") return String(id);
37
+ else throw new Error(`Unsupported FSId type: ${typeof id}`);
38
+ }
39
+ function isIdsEqual(id1, id2) {
40
+ return idToString(id1) === idToString(id2);
41
+ }
42
+ function parseBillingCycle(cycle) {
43
+ const billingCycle = Number.parseInt(cycle?.toString() ?? "", 10);
44
+ if (billingCycle === 1) return BILLING_CYCLE.MONTHLY;
45
+ if (billingCycle === 12) return BILLING_CYCLE.YEARLY;
46
+ return BILLING_CYCLE.ONEOFF;
47
+ }
48
+ function parseNumber(value) {
49
+ if (typeof value === "number") return value;
50
+ else if (typeof value === "string") {
51
+ const parsed = Number.parseFloat(value);
52
+ return Number.isNaN(parsed) ? null : parsed;
53
+ } else return null;
54
+ }
55
+ function parseDateTime(dateString) {
56
+ if (!dateString) return null;
57
+ const dateParts = dateString.split(" ");
58
+ if (dateParts.length !== 2) return null;
59
+ const date = dateParts[0].split("-");
60
+ const time = dateParts[1].split(":");
61
+ if (date.length !== 3 || time.length !== 3) return null;
62
+ const year = Number.parseInt(date[0]);
63
+ const month = Number.parseInt(date[1]) - 1;
64
+ const day = Number.parseInt(date[2]);
65
+ const hours = Number.parseInt(time[0]);
66
+ const minutes = Number.parseInt(time[1]);
67
+ const seconds = Number.parseInt(time[2]);
68
+ if (Number.isNaN(year) || Number.isNaN(month) || Number.isNaN(day) || Number.isNaN(hours) || Number.isNaN(minutes) || Number.isNaN(seconds)) return null;
69
+ const utcDate = new Date(Date.UTC(year, month, day, hours, minutes, seconds, 0));
70
+ return utcDate;
71
+ }
72
+ function parseDate(dateString) {
73
+ if (!dateString) return null;
74
+ return parseDateTime(dateString + " 00:00:00");
75
+ }
76
+ function parseCurrency(currency) {
77
+ switch (currency?.toLowerCase?.()) {
78
+ case "usd": return CURRENCY.USD;
79
+ case "eur": return CURRENCY.EUR;
80
+ case "gbp": return CURRENCY.GBP;
81
+ default: return null;
82
+ }
83
+ }
84
+ function parsePaymentMethod(gateway) {
85
+ return gateway?.startsWith("stripe") ? "card" : gateway?.startsWith("paypal") ? "paypal" : null;
86
+ }
87
+
88
+ //#endregion
89
+ //#region src/api/client.ts
90
+ function createApiClient(baseUrl, bearerToken) {
91
+ return createClient({
92
+ baseUrl,
93
+ headers: { Authorization: bearerToken ? `Bearer ${bearerToken}` : void 0 }
94
+ });
95
+ }
96
+
97
+ //#endregion
98
+ //#region src/api/ApiBase.ts
99
+ const PAGING_DEFAULT_LIMIT = 150;
100
+ const defaultPagingOptions = {
101
+ count: PAGING_DEFAULT_LIMIT,
102
+ offset: 0
103
+ };
104
+ var ApiBase = class {
105
+ productId;
106
+ constructor(productId, client) {
107
+ this.client = client;
108
+ this.productId = idToNumber(productId);
109
+ }
110
+ /**
111
+ * Async generator that yields all entities by paginating through retrieveMany.
112
+ * @param filter Optional filter for entities
113
+ * @param pageSize Optional page size (default: PAGING_DEFAULT_LIMIT)
114
+ *
115
+ * @example
116
+ * // Usage example:
117
+ * for await (const entity of apiInstance.iterateAll({ status: 'active' })) {
118
+ * console.log(entity);
119
+ * }
120
+ */
121
+ async *iterateAll(filter, pageSize = PAGING_DEFAULT_LIMIT) {
122
+ let offset = 0;
123
+ while (true) {
124
+ const page = await this.retrieveMany(filter, {
125
+ count: pageSize,
126
+ offset
127
+ });
128
+ if (!page.length) break;
129
+ for (const entity of page) yield entity;
130
+ if (page.length < pageSize) break;
131
+ offset += page.length;
132
+ }
133
+ }
134
+ getPagingParams(paging = defaultPagingOptions) {
135
+ return {
136
+ count: paging.count ?? PAGING_DEFAULT_LIMIT,
137
+ offset: paging.offset ?? 0
138
+ };
139
+ }
140
+ getIdForPath(id) {
141
+ return idToNumber(id);
142
+ }
143
+ isGoodResponse(response) {
144
+ return response.status >= 200 && response.status < 300;
145
+ }
146
+ };
147
+
148
+ //#endregion
149
+ //#region src/api/License.ts
150
+ var License = class extends ApiBase {
151
+ async retrieve(licenseId) {
152
+ const licenseResponse = await this.client.GET(`/products/{product_id}/licenses/{license_id}.json`, { params: { path: {
153
+ product_id: this.productId,
154
+ license_id: this.getIdForPath(licenseId)
155
+ } } });
156
+ if (!this.isGoodResponse(licenseResponse.response) || !licenseResponse.data || !licenseResponse.data.id) return null;
157
+ return licenseResponse.data;
158
+ }
159
+ async retrieveMany(filter, pagination) {
160
+ const response = await this.client.GET(`/products/{product_id}/licenses.json`, { params: {
161
+ path: { product_id: this.productId },
162
+ query: {
163
+ ...this.getPagingParams(pagination),
164
+ ...filter ?? {}
165
+ }
166
+ } });
167
+ if (!this.isGoodResponse(response.response) || !response.data || !Array.isArray(response.data.licenses)) return [];
168
+ return response.data.licenses;
169
+ }
170
+ async retrieveSubscription(licenseId) {
171
+ const subscriptionResponse = await this.client.GET(`/products/{product_id}/licenses/{license_id}/subscription.json`, { params: { path: {
172
+ product_id: this.productId,
173
+ license_id: this.getIdForPath(licenseId)
174
+ } } });
175
+ if (!this.isGoodResponse(subscriptionResponse.response) || !subscriptionResponse.data || !subscriptionResponse.data.id) return null;
176
+ return subscriptionResponse.data;
177
+ }
178
+ async retrieveCheckoutUpgradeAuthorization(licenseId) {
179
+ const response = await this.client.POST(`/products/{product_id}/licenses/{license_id}/checkout/link.json`, {
180
+ params: { path: {
181
+ product_id: this.productId,
182
+ license_id: this.getIdForPath(licenseId)
183
+ } },
184
+ body: { is_payment_method_update: true }
185
+ });
186
+ if (!this.isGoodResponse(response.response) || !response.data || !response.data.settings) return null;
187
+ return response.data.settings.authorization;
188
+ }
189
+ };
190
+
191
+ //#endregion
192
+ //#region src/api/Product.ts
193
+ var Product = class extends ApiBase {
194
+ async retrieve() {
195
+ const response = await this.client.GET(`/products/{product_id}.json`, { params: { path: { product_id: this.productId } } });
196
+ if (response.response.status !== 200 || !response.data) return null;
197
+ return response.data;
198
+ }
199
+ async retrieveMany() {
200
+ throw new Error("retrieveMany is not supported for Product API");
201
+ }
202
+ async retrievePricingData() {
203
+ const response = await this.client.GET(`/products/{product_id}/pricing.json`, { params: { path: { product_id: this.productId } } });
204
+ if (response.response.status !== 200 || !response.data) return null;
205
+ return response.data;
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ //#region src/api/Subscription.ts
211
+ var Subscription = class extends ApiBase {
212
+ async retrieve(subscriptionId) {
213
+ const response = await this.client.GET(`/products/{product_id}/subscriptions/{subscription_id}.json`, { params: { path: {
214
+ product_id: this.productId,
215
+ subscription_id: this.getIdForPath(subscriptionId)
216
+ } } });
217
+ if (response.response.status !== 200 || !response.data || !response.data.id) return null;
218
+ return response.data;
219
+ }
220
+ async retrieveMany(filter, pagination) {
221
+ const response = await this.client.GET(`/products/{product_id}/subscriptions.json`, { params: {
222
+ path: { product_id: this.productId },
223
+ query: {
224
+ ...this.getPagingParams(pagination),
225
+ ...filter ?? {}
226
+ }
227
+ } });
228
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
229
+ return response.data.subscriptions;
230
+ }
231
+ };
232
+
233
+ //#endregion
234
+ //#region src/api/User.ts
235
+ const USER_FIELDS = "email,first,last,picture,is_verified,id,created,updated,is_marketing_allowed";
236
+ var User = class extends ApiBase {
237
+ async retrieve(userId) {
238
+ const userResponse = await this.client.GET(`/products/{product_id}/users/{user_id}.json`, { params: {
239
+ path: {
240
+ product_id: this.productId,
241
+ user_id: this.getIdForPath(userId)
242
+ },
243
+ query: { fields: USER_FIELDS }
244
+ } });
245
+ if (userResponse.response.status !== 200 || !userResponse.data || !userResponse.data.id) return null;
246
+ return userResponse.data;
247
+ }
248
+ async retrieveMany(filter, pagination) {
249
+ const response = await this.client.GET(`/products/{product_id}/users.json`, { params: {
250
+ path: { product_id: this.productId },
251
+ query: {
252
+ ...this.getPagingParams(pagination),
253
+ ...filter ?? {},
254
+ fields: USER_FIELDS
255
+ }
256
+ } });
257
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.users)) return [];
258
+ return response.data.users;
259
+ }
260
+ async retrieveByEmail(email) {
261
+ const response = await this.client.GET(`/products/{product_id}/users.json`, { params: {
262
+ path: { product_id: this.productId },
263
+ query: { email }
264
+ } });
265
+ if (!this.isGoodResponse(response.response) || !Array.isArray(response.data?.users)) return null;
266
+ return response.data.users?.[0] ?? null;
267
+ }
268
+ async retrieveBilling(userId) {
269
+ const billingResponse = await this.client.GET(`/products/{product_id}/users/{user_id}/billing.json`, { params: { path: {
270
+ product_id: this.productId,
271
+ user_id: this.getIdForPath(userId)
272
+ } } });
273
+ if (billingResponse.response.status !== 200 || !billingResponse.data || !billingResponse.data) return null;
274
+ return billingResponse.data;
275
+ }
276
+ async retrieveSubscriptions(userId, filters, pagination) {
277
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/subscriptions.json`, { params: {
278
+ path: {
279
+ product_id: this.productId,
280
+ user_id: this.getIdForPath(userId)
281
+ },
282
+ query: {
283
+ ...filters ?? {},
284
+ ...this.getPagingParams(pagination)
285
+ }
286
+ } });
287
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.subscriptions)) return [];
288
+ return response.data.subscriptions;
289
+ }
290
+ async retrieveLicenses(userId, filters, pagination) {
291
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/licenses.json`, { params: {
292
+ path: {
293
+ product_id: this.productId,
294
+ user_id: this.getIdForPath(userId)
295
+ },
296
+ query: {
297
+ ...filters ?? {},
298
+ ...this.getPagingParams(pagination)
299
+ }
300
+ } });
301
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.licenses)) return [];
302
+ return response.data.licenses;
303
+ }
304
+ async retrievePayments(userId, filters, pagination) {
305
+ const response = await this.client.GET(`/products/{product_id}/users/{user_id}/payments.json`, { params: {
306
+ path: {
307
+ product_id: this.productId,
308
+ user_id: this.getIdForPath(userId)
309
+ },
310
+ query: {
311
+ ...filters ?? {},
312
+ ...this.getPagingParams(pagination)
313
+ }
314
+ } });
315
+ if (response.response.status !== 200 || !response.data || !Array.isArray(response.data.payments)) return [];
316
+ return response.data.payments;
317
+ }
318
+ };
319
+
320
+ //#endregion
321
+ //#region src/services/ApiService.ts
322
+ const API_ENDPOINT_PRODUCTION = "https://api.freemius.com/v1/";
323
+ const API_ENDPOINT_TEST = "http://api.freemius-local.com:8080/v1/";
324
+ var ApiService = class {
325
+ client;
326
+ productId;
327
+ user;
328
+ license;
329
+ product;
330
+ subscription;
331
+ baseUrl;
332
+ constructor(productId, apiKey, secretKey, publicKey) {
333
+ this.secretKey = secretKey;
334
+ this.publicKey = publicKey;
335
+ const isTestServer = process.env.FS__INTERNAL__IS_DEVELOPMENT_MODE === "true";
336
+ this.baseUrl = isTestServer ? API_ENDPOINT_TEST : API_ENDPOINT_PRODUCTION;
337
+ this.client = createApiClient(this.baseUrl, apiKey);
338
+ this.productId = idToString(productId);
339
+ this.user = new User(this.productId, this.client);
340
+ this.license = new License(this.productId, this.client);
341
+ this.product = new Product(this.productId, this.client);
342
+ this.subscription = new Subscription(this.productId, this.client);
343
+ }
344
+ /**
345
+ * Low level API client for direct access to the Freemius API.
346
+ * Use this for advanced use cases where you need to make custom API calls.
347
+ *
348
+ * For regular operations, prefer using the provided services like `User`, `Subscription`, `License` etc.
349
+ */
350
+ get __unstable_ApiClient() {
351
+ return this.client;
352
+ }
353
+ createUrl(path) {
354
+ path = path.replace(/^\/+/, "");
355
+ return `${this.baseUrl}products/${this.productId}/${path}`;
356
+ }
357
+ /**
358
+ * Generate signed URL for the given full URL
359
+ */
360
+ getSignedUrl(fullUrl) {
361
+ const url = new URL(fullUrl);
362
+ const resourcePath = url.pathname;
363
+ const auth = this.generateAuthorizationParams(resourcePath);
364
+ url.searchParams.set("auth_date", auth.date);
365
+ url.searchParams.set("authorization", auth.authorization);
366
+ return url.toString();
367
+ }
368
+ /**
369
+ * Generate authorization parameters for signing
370
+ */
371
+ generateAuthorizationParams(resourcePath, method = "GET", jsonEncodedParams = "", contentType = "") {
372
+ const eol = "\n";
373
+ let contentMd5 = "";
374
+ const date = this.toDateTimeString(/* @__PURE__ */ new Date());
375
+ if (["POST", "PUT"].includes(method) && jsonEncodedParams) contentMd5 = crypto$1.createHash("md5").update(jsonEncodedParams).digest("hex");
376
+ const stringToSign = [
377
+ method,
378
+ contentMd5,
379
+ contentType,
380
+ date,
381
+ resourcePath
382
+ ].join(eol);
383
+ const authType = this.secretKey !== this.publicKey ? "FS" : "FSP";
384
+ const signature = crypto$1.createHmac("sha256", this.secretKey).update(stringToSign).digest("hex");
385
+ const base64 = this.base64UrlEncode(signature);
386
+ return {
387
+ date,
388
+ authorization: `${authType} ${this.productId}:${this.publicKey}:${base64}`
389
+ };
390
+ }
391
+ /**
392
+ * Base64 encoding that doesn't need to be urlencode()ed.
393
+ * Exactly the same as base64_encode except it uses
394
+ * - instead of +
395
+ * _ instead of /
396
+ */
397
+ base64UrlEncode(input) {
398
+ return Buffer.from(input, "utf8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
399
+ }
400
+ toDateTimeString(date) {
401
+ const year = date.getUTCFullYear();
402
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
403
+ const day = String(date.getUTCDate()).padStart(2, "0");
404
+ const hours = String(date.getUTCHours()).padStart(2, "0");
405
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
406
+ const seconds = String(date.getUTCSeconds()).padStart(2, "0");
407
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
408
+ }
409
+ };
410
+
411
+ //#endregion
412
+ //#region src/utils/ops.ts
413
+ function splitName(name) {
414
+ const parts = name.split(" ");
415
+ return {
416
+ firstName: parts[0] ?? "",
417
+ lastName: parts.slice(1).join(" ") ?? ""
418
+ };
419
+ }
420
+
421
+ //#endregion
422
+ //#region src/models/CheckoutBuilder.ts
423
+ /**
424
+ * A builder class for constructing checkout parameters. This class provides a fluent
425
+ * API to create Checkout parameters for a product with various configurations.
426
+ *
427
+ *
428
+ *
429
+ * Every method returns a new instance of the builder with the updated options,
430
+ * allowing for method chaining. The final `toOptions()` method returns the constructed
431
+ * `CheckoutOptions` object. So the class itself is immutable and does not modify the original instance.
432
+ */
433
+ var CheckoutBuilder = class CheckoutBuilder {
434
+ constructor(options, productId, publicKey, secretKey) {
435
+ this.options = options;
436
+ this.productId = productId;
437
+ this.publicKey = publicKey;
438
+ this.secretKey = secretKey;
439
+ }
440
+ /**
441
+ * Enables sandbox mode for testing purposes.
442
+ *
443
+ * @returns A new builder instance with sandbox configuration
444
+ */
445
+ inSandbox() {
446
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
447
+ const token = `${timestamp}${this.productId}${this.secretKey}${this.publicKey}checkout`;
448
+ return new CheckoutBuilder({
449
+ ...this.options,
450
+ sandbox: {
451
+ ctx: timestamp,
452
+ token: createHash("md5").update(token).digest("hex")
453
+ }
454
+ }, this.productId, this.publicKey, this.secretKey);
455
+ }
456
+ /**
457
+ * Sets user information for the checkout session.
458
+ *
459
+ * @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.
460
+ * @param readonly If true, the user information will be read-only in the checkout session.
461
+ *
462
+ * @returns A new builder instance with user configuration
463
+ */
464
+ withUser(user, readonly = true) {
465
+ if (!user) return this;
466
+ let firstName = user.firstName ?? "";
467
+ let lastName = user.lastName ?? "";
468
+ if (user.name) {
469
+ const { firstName: fn, lastName: ln } = splitName(user.name);
470
+ firstName = fn;
471
+ lastName = ln;
472
+ }
473
+ return new CheckoutBuilder({
474
+ ...this.options,
475
+ user_email: user.email,
476
+ user_first_name: firstName,
477
+ user_last_name: lastName,
478
+ readonly_user: readonly
479
+ }, this.productId, this.publicKey, this.secretKey);
480
+ }
481
+ /**
482
+ * Applies recommended UI settings for better user experience.
483
+ * This includes fullscreen mode, upsells, refund badge, and reviews display.
484
+ *
485
+ * @returns A new builder instance with recommended UI settings
486
+ */
487
+ withRecommendation() {
488
+ return new CheckoutBuilder({
489
+ ...this.options,
490
+ fullscreen: true,
491
+ show_refund_badge: true,
492
+ show_reviews: true,
493
+ locale: "auto",
494
+ currency: "auto"
495
+ }, this.productId, this.publicKey, this.secretKey);
496
+ }
497
+ /**
498
+ * Sets the plan ID for the checkout.
499
+ *
500
+ * @param planId The plan ID to purchase
501
+ * @returns A new builder instance with plan ID set
502
+ */
503
+ withPlan(planId) {
504
+ return new CheckoutBuilder({
505
+ ...this.options,
506
+ plan_id: planId.toString()
507
+ }, this.productId, this.publicKey, this.secretKey);
508
+ }
509
+ /**
510
+ * Sets the number of licenses to purchase.
511
+ *
512
+ * @param count Number of licenses
513
+ * @returns A new builder instance with license count set
514
+ */
515
+ withQuota(count) {
516
+ return new CheckoutBuilder({
517
+ ...this.options,
518
+ licenses: count
519
+ }, this.productId, this.publicKey, this.secretKey);
520
+ }
521
+ withPricing(pricingId) {
522
+ return new CheckoutBuilder({
523
+ ...this.options,
524
+ pricing_id: pricingId.toString()
525
+ }, this.productId, this.publicKey, this.secretKey);
526
+ }
527
+ withTitle(title) {
528
+ return new CheckoutBuilder({
529
+ ...this.options,
530
+ title
531
+ }, this.productId, this.publicKey, this.secretKey);
532
+ }
533
+ /**
534
+ * Sets a coupon code for the checkout.
535
+ *
536
+ * @param coupon The coupon code to apply
537
+ * @param hideUI Whether to hide the coupon input field from users
538
+ * @returns A new builder instance with coupon configuration
539
+ */
540
+ withCoupon(options) {
541
+ const { code: coupon, hideUI = false } = options;
542
+ const newOptions = {
543
+ ...this.options,
544
+ hide_coupon: hideUI
545
+ };
546
+ if (coupon !== void 0) newOptions.coupon = coupon;
547
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
548
+ }
549
+ /**
550
+ * Enables trial mode for the checkout.
551
+ *
552
+ * @param mode Trial type - true/false for plan default, or specific 'free'/'paid' mode
553
+ * @returns A new builder instance with trial configuration
554
+ */
555
+ inTrial(mode = true) {
556
+ return new CheckoutBuilder({
557
+ ...this.options,
558
+ trial: mode
559
+ }, this.productId, this.publicKey, this.secretKey);
560
+ }
561
+ /**
562
+ * Configures the visual layout and appearance of the checkout.
563
+ *
564
+ * @param options Appearance configuration options
565
+ * @returns A new builder instance with appearance configuration
566
+ */
567
+ withAppearance(options) {
568
+ const newOptions = { ...this.options };
569
+ if (options.layout !== void 0) newOptions.layout = options.layout;
570
+ if (options.formPosition !== void 0) newOptions.form_position = options.formPosition;
571
+ if (options.fullscreen !== void 0) newOptions.fullscreen = options.fullscreen;
572
+ if (options.modalTitle !== void 0) newOptions.modal_title = options.modalTitle;
573
+ if (options.id !== void 0) newOptions.id = options.id;
574
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
575
+ }
576
+ /**
577
+ * Configures discount display settings.
578
+ *
579
+ * @param options Discount configuration options
580
+ * @returns A new builder instance with discount configuration
581
+ */
582
+ withDiscounts(options) {
583
+ const newOptions = { ...this.options };
584
+ if (options.annual !== void 0) newOptions.annual_discount = options.annual;
585
+ if (options.multisite !== void 0) newOptions.multisite_discount = options.multisite;
586
+ if (options.bundle !== void 0) newOptions.bundle_discount = options.bundle;
587
+ if (options.showMonthlySwitch !== void 0) newOptions.show_monthly_switch = options.showMonthlySwitch;
588
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
589
+ }
590
+ /**
591
+ * Configures billing cycle selector interface.
592
+ *
593
+ * @param selector Type of billing cycle selector to show
594
+ * @param defaultCycle Default billing cycle to select
595
+ * @returns A new builder instance with billing cycle configuration
596
+ */
597
+ withBillingCycle(defaultCycle, selector) {
598
+ const newOptions = { ...this.options };
599
+ if (selector !== void 0) newOptions.billing_cycle_selector = selector;
600
+ if (defaultCycle !== void 0) newOptions.billing_cycle = defaultCycle;
601
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
602
+ }
603
+ /**
604
+ * Sets the language/locale for the checkout.
605
+ *
606
+ * @param locale Language setting - 'auto', 'auto-beta', or specific locale like 'en_US'
607
+ * @returns A new builder instance with locale configuration
608
+ */
609
+ withLanguage(locale = "auto") {
610
+ return new CheckoutBuilder({
611
+ ...this.options,
612
+ language: locale,
613
+ locale
614
+ }, this.productId, this.publicKey, this.secretKey);
615
+ }
616
+ /**
617
+ * Configures review and badge display settings.
618
+ *
619
+ * @param options Review and badge configuration
620
+ * @returns A new builder instance with reviews and badges configuration
621
+ */
622
+ withReviewsAndBadges(options) {
623
+ const newOptions = { ...this.options };
624
+ if (options.showReviews !== void 0) newOptions.show_reviews = options.showReviews;
625
+ if (options.reviewId !== void 0) newOptions.review_id = options.reviewId;
626
+ if (options.showRefundBadge !== void 0) newOptions.show_refund_badge = options.showRefundBadge;
627
+ if (options.refundPolicyPosition !== void 0) newOptions.refund_policy_position = options.refundPolicyPosition;
628
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
629
+ }
630
+ /**
631
+ * Enhanced currency configuration.
632
+ *
633
+ * @param currency Primary currency or 'auto' for automatic detection
634
+ * @param defaultCurrency Default currency when using 'auto'
635
+ * @param showInlineSelector Whether to show inline currency selector
636
+ * @returns A new builder instance with currency configuration
637
+ */
638
+ withCurrency(currency, defaultCurrency = "usd", showInlineSelector = true) {
639
+ const options = {
640
+ ...this.options,
641
+ show_inline_currency_selector: showInlineSelector,
642
+ default_currency: defaultCurrency
643
+ };
644
+ if (currency !== "auto") options.currency = currency;
645
+ return new CheckoutBuilder(options, this.productId, this.publicKey, this.secretKey);
646
+ }
647
+ /**
648
+ * Configures navigation and cancel behavior.
649
+ *
650
+ * @param cancelUrl URL for back button when in page mode
651
+ * @param cancelIcon Custom cancel icon URL
652
+ * @returns A new builder instance with navigation configuration
653
+ */
654
+ withNavigation(cancelUrl, cancelIcon) {
655
+ const newOptions = { ...this.options };
656
+ if (cancelUrl !== void 0) newOptions.cancel_url = cancelUrl;
657
+ if (cancelIcon !== void 0) newOptions.cancel_icon = cancelIcon;
658
+ return new CheckoutBuilder(newOptions, this.productId, this.publicKey, this.secretKey);
659
+ }
660
+ /**
661
+ * Associates purchases with an affiliate account.
662
+ *
663
+ * @param userId Affiliate user ID
664
+ * @returns A new builder instance with affiliate configuration
665
+ */
666
+ withAffiliate(userId) {
667
+ return new CheckoutBuilder({
668
+ ...this.options,
669
+ affiliate_user_id: userId
670
+ }, this.productId, this.publicKey, this.secretKey);
671
+ }
672
+ /**
673
+ * Sets a custom image/icon for the checkout.
674
+ *
675
+ * @param imageUrl Secure HTTPS URL to the image
676
+ * @returns A new builder instance with custom image
677
+ */
678
+ withImage(imageUrl) {
679
+ return new CheckoutBuilder({
680
+ ...this.options,
681
+ image: imageUrl
682
+ }, this.productId, this.publicKey, this.secretKey);
683
+ }
684
+ /**
685
+ * Configures the checkout for license renewal.
686
+ *
687
+ * @param licenseKey The license key to renew
688
+ * @returns A new builder instance configured for renewal
689
+ */
690
+ forRenewal(licenseKey) {
691
+ return new CheckoutBuilder({
692
+ ...this.options,
693
+ license_key: licenseKey
694
+ }, this.productId, this.publicKey, this.secretKey);
695
+ }
696
+ /**
697
+ * Builds and returns the final checkout options to be used with the `@freemius/checkout` package.
698
+ *
699
+ * @returns The constructed CheckoutOptions object
700
+ */
701
+ toOptions(additionalOptions) {
702
+ return {
703
+ ...this.options,
704
+ ...additionalOptions,
705
+ product_id: this.productId
706
+ };
707
+ }
708
+ toLink() {
709
+ const checkoutOptions = convertCheckoutOptionsToQueryParams(this.options);
710
+ const queryParams = buildFreemiusQueryFromOptions(checkoutOptions);
711
+ const url = new URL(`https://checkout.freemius.com/product/${this.productId}/`);
712
+ url.search = queryParams;
713
+ return url.href;
714
+ }
715
+ };
716
+
717
+ //#endregion
718
+ //#region src/models/CheckoutRedirectInfo.ts
719
+ var CheckoutRedirectInfo = class {
720
+ user_id;
721
+ plan_id;
722
+ email;
723
+ pricing_id;
724
+ currency;
725
+ license_id;
726
+ expiration;
727
+ quota;
728
+ action;
729
+ amount;
730
+ tax;
731
+ type;
732
+ subscription_id;
733
+ billing_cycle;
734
+ payment_id;
735
+ constructor(data) {
736
+ this.user_id = idToString(data.user_id);
737
+ this.plan_id = idToString(data.plan_id);
738
+ this.email = data.email;
739
+ this.pricing_id = idToString(data.pricing_id);
740
+ this.currency = data.currency ? parseCurrency(data.currency) : CURRENCY.USD;
741
+ this.license_id = idToString(data.license_id);
742
+ this.expiration = data.expiration ? parseDateTime(data.expiration) : null;
743
+ this.quota = data.quota ? parseNumber(data.quota) : null;
744
+ this.action = data.action ? data.action : null;
745
+ this.amount = parseNumber(data.amount);
746
+ this.tax = parseNumber(data.tax);
747
+ this.type = data.type === "subscription" ? "subscription" : "one-off";
748
+ this.subscription_id = data.subscription_id ? idToString(data.subscription_id) : null;
749
+ this.billing_cycle = data.billing_cycle ? parseBillingCycle(data.billing_cycle) : null;
750
+ this.payment_id = data.payment_id ? idToString(data.payment_id) : null;
751
+ }
752
+ isSubscription() {
753
+ return this.type === "subscription";
754
+ }
755
+ toData() {
756
+ return {
757
+ user_id: this.user_id,
758
+ plan_id: this.plan_id,
759
+ email: this.email,
760
+ pricing_id: this.pricing_id,
761
+ currency: this.currency,
762
+ license_id: this.license_id,
763
+ expiration: this.expiration,
764
+ quota: this.quota,
765
+ action: this.action,
766
+ amount: this.amount,
767
+ tax: this.tax,
768
+ type: this.type,
769
+ subscription_id: this.subscription_id,
770
+ billing_cycle: this.billing_cycle,
771
+ payment_id: this.payment_id
772
+ };
773
+ }
774
+ };
775
+
776
+ //#endregion
777
+ //#region src/services/CheckoutService.ts
778
+ var CheckoutService = class {
779
+ constructor(productId, publicKey, secretKey) {
780
+ this.productId = productId;
781
+ this.publicKey = publicKey;
782
+ this.secretKey = secretKey;
783
+ }
784
+ /**
785
+ * Use this to build a Checkout for your product.
786
+ * You can build a Checkout link or options for the popup.
787
+ *
788
+ * @param withRecommendation If true, the checkout will include a recommendation for the user.
789
+ *
790
+ * @return A new instance of CheckoutBuilder with the product ID and public key.
791
+ *
792
+ * @example
793
+ * Basic usage:
794
+ * ```typescript
795
+ * const options = freemius.checkout.params()
796
+ * .withUser(session?.user)
797
+ * .inSandbox()
798
+ * .withRecommendation()
799
+ * .toOptions(); // Or .toLink() for a hosted checkout link
800
+ * ```
801
+ *
802
+ * @example
803
+ * Advanced configuration:
804
+ * ```typescript
805
+ * const checkoutOptions = freemius.checkout.params()
806
+ * .withUser(user, true)
807
+ * .withPlan('1234')
808
+ * .withQuota(5)
809
+ * .withCurrency('eur')
810
+ * .withCoupon({
811
+ * code: 'DISCOUNT2023',
812
+ * hideUI: false
813
+ * })
814
+ * .inTrial('paid')
815
+ * .withAppearance({
816
+ * layout: 'horizontal',
817
+ * formPosition: 'left',
818
+ * fullscreen: true,
819
+ * modalTitle: 'Upgrade Now'
820
+ * })
821
+ * .withDiscounts({
822
+ * annual: true,
823
+ * multisite: 'auto',
824
+ * bundle: 'maximize',
825
+ * showMonthlySwitch: true
826
+ * })
827
+ * .withReviewsAndBadges({
828
+ * showReviews: true,
829
+ * showRefundBadge: true,
830
+ * refundPolicyPosition: 'below_form'
831
+ * })
832
+ * .withBillingCycle('dropdown', 'annual')
833
+ * .withLocale('en_US')
834
+ * .withAffiliate(12345)
835
+ * .inSandbox()
836
+ * .toOptions();
837
+ * ```
838
+ */
839
+ create(withRecommendation = true) {
840
+ const productId = idToString(this.productId);
841
+ const builder = new CheckoutBuilder({}, productId, this.publicKey, this.secretKey);
842
+ return withRecommendation ? builder.withRecommendation() : builder;
843
+ }
844
+ /**
845
+ * Convenience method to create checkout options for a specific user with or without sandbox mode.
846
+ *
847
+ * Useful for generating recommended checkout options for SaaS.
848
+ */
849
+ createUserOptions(user, isSandbox = false) {
850
+ let builder = this.create().withUser(user);
851
+ if (isSandbox) builder = builder.inSandbox();
852
+ return builder.toOptions();
853
+ }
854
+ /**
855
+ * Convenience method to create a checkout link for a specific user with or without sandbox mode.
856
+ *
857
+ * Useful for generating recommended checkout links for SaaS.
858
+ */
859
+ createUserLink(user, isSandbox = false) {
860
+ let builder = this.create().withUser(user);
861
+ if (isSandbox) builder = builder.inSandbox();
862
+ return builder.toLink();
863
+ }
864
+ /**
865
+ * Retrieves the sandbox parameters for the checkout.
866
+ *
867
+ * This shouldn't be used in production, but is useful for testing purposes.
868
+ *
869
+ * @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).
870
+ *
871
+ * @todo - This has a duplication with the `inSandbox` method in the builder. Consider refactoring to avoid this duplication.
872
+ * Also think about whether we should make the builder's `inSandbox` method async as well.
873
+ */
874
+ async getSandboxParams() {
875
+ const timestamp = Math.floor(Date.now() / 1e3).toString();
876
+ const token = `${timestamp}${this.productId}${this.secretKey}${this.publicKey}checkout`;
877
+ return {
878
+ ctx: timestamp,
879
+ token: createHash("md5").update(token).digest("hex")
880
+ };
881
+ }
882
+ /**
883
+ * Processes the redirect from Freemius Checkout.
884
+ *
885
+ * This method verifies the signature in the URL and returns a CheckoutRedirectInfo object if successful.
886
+ *
887
+ * For nextjs like applications, make sure to replace the URL from the `Request` object with the right hostname to take care of the proxy.
888
+ *
889
+ * For example, if you have put the nextjs application behind nginx proxy (or ngrok during local development), then nextjs will still see the `request.url` as `https://localhost:3000/...`.
890
+ * In this case, you should replace it with the actual URL of your application, like `https://xyz.ngrok-free.app/...`.
891
+ *
892
+ * @example
893
+ * ```ts
894
+ * export async function GET(request: Request) {
895
+ * // Replace the URL with the actual hostname of your application
896
+ * // This is important for the signature verification to work correctly.
897
+ * const data = await freemius.checkout.processRedirect(
898
+ * request.url.replace('https://localhost:3000', 'https://xyz.ngrok-free.app')
899
+ * );
900
+ * }
901
+ * ```
902
+ */
903
+ async processRedirect(currentUrl) {
904
+ const url = new URL(currentUrl.replace(/%20/g, "+"));
905
+ const signature = url.searchParams.get("signature");
906
+ if (!signature) return null;
907
+ const cleanUrl = this.getCleanUrl(url.href);
908
+ const calculatedSignature = createHmac("sha256", this.secretKey).update(cleanUrl).digest("hex");
909
+ const result = timingSafeEqual(Buffer.from(calculatedSignature), Buffer.from(signature));
910
+ if (!result) return null;
911
+ const params = Object.fromEntries(url.searchParams.entries());
912
+ if (!params.user_id || !params.plan_id || !params.pricing_id || !params.email) return null;
913
+ return new CheckoutRedirectInfo(params);
914
+ }
915
+ getCleanUrl(url) {
916
+ const signatureParam = "&signature=";
917
+ const signatureParamFirst = "?signature=";
918
+ let signaturePos = url.indexOf(signatureParam);
919
+ if (signaturePos === -1) signaturePos = url.indexOf(signatureParamFirst);
920
+ if (signaturePos === -1) return url;
921
+ return url.substring(0, signaturePos);
922
+ }
923
+ };
924
+
925
+ //#endregion
926
+ //#region src/services/CustomerPortalService.ts
927
+ var CustomerPortalService = class {
928
+ constructor(api, checkout) {
929
+ this.api = api;
930
+ this.checkout = checkout;
931
+ }
932
+ /**
933
+ * Retrieves the customer portal data for a user, including subscriptions, billing, and payments.
934
+ *
935
+ * @param userId The ID of the user for whom to retrieve portal data.
936
+ * @param primaryLicenseId Optional primary license ID to include in the portal data. If present then the `primary` field will be populated with related information which our `@freemius/saas-starter` package uses to display the primary purchase information.
937
+ */
938
+ async retrieveData(userId, primaryLicenseId = null, sandbox = false) {
939
+ const [user, pricingData, subscriptions, payments, billing] = await Promise.all([
940
+ this.api.user.retrieve(userId),
941
+ this.api.product.retrievePricingData(),
942
+ this.api.user.retrieveSubscriptions(userId),
943
+ this.api.user.retrievePayments(userId),
944
+ this.api.user.retrieveBilling(userId)
945
+ ]);
946
+ if (!user || !pricingData || !subscriptions) return null;
947
+ const planTitles = this.getPlanTitleById(pricingData);
948
+ const allPricingsById = this.getPricingById(pricingData);
949
+ const portalPayments = payments.map((payment) => ({
950
+ ...payment,
951
+ invoiceUrl: this.api.getSignedUrl(this.api.createUrl(`payments/${payment.id}/invoice.pdf`)),
952
+ paymentMethod: parsePaymentMethod(payment.gateway),
953
+ createdAt: parseDateTime(payment.created) ?? /* @__PURE__ */ new Date(),
954
+ planTitle: planTitles[payment.plan_id] ?? `Plan ${payment.plan_id}`,
955
+ quota: allPricingsById[payment.pricing_id]?.licenses ?? null
956
+ }));
957
+ const checkoutOptions = { product_id: this.api.productId };
958
+ if (sandbox) checkoutOptions.sandbox = await this.checkout.getSandboxParams();
959
+ const billingData = {
960
+ user,
961
+ checkoutOptions,
962
+ billing,
963
+ subscriptions: {
964
+ primary: null,
965
+ active: [],
966
+ past: []
967
+ },
968
+ payments: portalPayments,
969
+ plans: pricingData.plans ?? [],
970
+ sellingUnit: pricingData.selling_unit_label ?? {
971
+ singular: "Unit",
972
+ plural: "Units"
973
+ },
974
+ productId: this.api.productId
975
+ };
976
+ subscriptions.forEach((subscription) => {
977
+ const isActive = null === subscription.canceled_at;
978
+ const subscriptionData = {
979
+ subscriptionId: idToString(subscription.id),
980
+ planId: idToString(subscription.plan_id),
981
+ pricingId: idToString(subscription.pricing_id),
982
+ planTitle: planTitles[subscription.plan_id] ?? `Plan ${subscription.plan_id}`,
983
+ renewalAmount: parseNumber(subscription.renewal_amount),
984
+ initialAmount: parseNumber(subscription.initial_amount),
985
+ billingCycle: parseBillingCycle(subscription.billing_cycle),
986
+ isActive,
987
+ renewalDate: parseDateTime(subscription.next_payment),
988
+ licenseId: idToString(subscription.license_id),
989
+ currency: parseCurrency(subscription.currency) ?? CURRENCY.USD,
990
+ createdAt: parseDateTime(subscription.created) ?? /* @__PURE__ */ new Date(),
991
+ cancelledAt: subscription.canceled_at ? parseDateTime(subscription.canceled_at) : null,
992
+ quota: allPricingsById[subscription.pricing_id]?.licenses ?? null,
993
+ paymentMethod: parsePaymentMethod(subscription.gateway)
994
+ };
995
+ if (isActive) billingData.subscriptions.active.push(subscriptionData);
996
+ else billingData.subscriptions.past.push(subscriptionData);
997
+ if (isActive && primaryLicenseId && isIdsEqual(subscription.license_id, primaryLicenseId)) billingData.subscriptions.primary = subscriptionData;
998
+ });
999
+ billingData.subscriptions.active.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1000
+ billingData.subscriptions.past.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
1001
+ if (!billingData.subscriptions.primary) billingData.subscriptions.primary = billingData.subscriptions.active[0] ?? billingData.subscriptions.past[0] ?? null;
1002
+ if (billingData.subscriptions.primary) billingData.subscriptions.primary.checkoutUpgradeAuthorization = await this.api.license.retrieveCheckoutUpgradeAuthorization(billingData.subscriptions.primary.licenseId);
1003
+ return billingData;
1004
+ }
1005
+ /**
1006
+ * @todo - Implement this method to handle actions like get cancel coupon, cancel subscription, update billing, get upgrade auth for Checkout etc.
1007
+ */
1008
+ getPlanTitleById(pricingData) {
1009
+ const planTitles = {};
1010
+ pricingData.plans?.forEach((plan) => {
1011
+ planTitles[plan.id] = plan.title ?? plan.name ?? "Unknown Plan";
1012
+ });
1013
+ return planTitles;
1014
+ }
1015
+ getPricingById(pricingData) {
1016
+ const pricing = {};
1017
+ pricingData.plans?.forEach((plan) => {
1018
+ plan.pricing?.forEach((p) => {
1019
+ pricing[p.id] = p;
1020
+ });
1021
+ });
1022
+ return pricing;
1023
+ }
1024
+ };
1025
+
1026
+ //#endregion
1027
+ //#region src/models/PurchaseInfo.ts
1028
+ var PurchaseInfo = class {
1029
+ email;
1030
+ firstName;
1031
+ lastName;
1032
+ userId;
1033
+ planId;
1034
+ pricingId;
1035
+ licenseId;
1036
+ expiration;
1037
+ canceled;
1038
+ subscriptionId;
1039
+ billingCycle;
1040
+ quota;
1041
+ initialAmount;
1042
+ renewalAmount;
1043
+ currency;
1044
+ renewalDate;
1045
+ paymentMethod;
1046
+ created;
1047
+ constructor(user, license, subscription) {
1048
+ this.email = user.email;
1049
+ this.firstName = user.first ?? "";
1050
+ this.lastName = user.last ?? "";
1051
+ this.userId = idToString(license.user_id);
1052
+ this.canceled = license.is_cancelled ?? false;
1053
+ this.expiration = license.expiration ? parseDateTime(license.expiration) : null;
1054
+ this.licenseId = idToString(license.id);
1055
+ this.planId = idToString(license.plan_id);
1056
+ this.subscriptionId = null;
1057
+ this.billingCycle = null;
1058
+ this.quota = license.quota ?? null;
1059
+ this.pricingId = idToString(license.pricing_id);
1060
+ this.initialAmount = null;
1061
+ this.renewalAmount = null;
1062
+ this.currency = null;
1063
+ this.renewalDate = null;
1064
+ this.paymentMethod = null;
1065
+ this.created = parseDateTime(license.created) ?? /* @__PURE__ */ new Date();
1066
+ if (subscription) {
1067
+ this.subscriptionId = idToString(subscription.id);
1068
+ this.billingCycle = parseBillingCycle(subscription.billing_cycle);
1069
+ this.initialAmount = parseNumber(subscription.initial_amount);
1070
+ this.renewalAmount = parseNumber(subscription.renewal_amount);
1071
+ this.currency = parseCurrency(subscription.currency);
1072
+ this.renewalDate = subscription.next_payment ? parseDateTime(subscription.next_payment) : null;
1073
+ this.paymentMethod = parsePaymentMethod(subscription.gateway);
1074
+ }
1075
+ }
1076
+ isPlan(planId) {
1077
+ return this.planId === idToString(planId);
1078
+ }
1079
+ isFromPlans(planIds) {
1080
+ return planIds.some((planId) => this.isPlan(planId));
1081
+ }
1082
+ toData() {
1083
+ return {
1084
+ email: this.email,
1085
+ firstName: this.firstName,
1086
+ lastName: this.lastName,
1087
+ userId: this.userId,
1088
+ planId: this.planId,
1089
+ pricingId: this.pricingId,
1090
+ licenseId: this.licenseId,
1091
+ expiration: this.expiration,
1092
+ canceled: this.canceled,
1093
+ subscriptionId: this.subscriptionId,
1094
+ billingCycle: this.billingCycle,
1095
+ quota: this.quota,
1096
+ isActive: this.isActive,
1097
+ initialAmount: this.initialAmount,
1098
+ renewalAmount: this.renewalAmount,
1099
+ currency: this.currency,
1100
+ renewalDate: this.renewalDate,
1101
+ paymentMethod: this.paymentMethod,
1102
+ created: this.created
1103
+ };
1104
+ }
1105
+ get isActive() {
1106
+ if (this.canceled) return false;
1107
+ if (this.expiration && this.expiration < /* @__PURE__ */ new Date()) return false;
1108
+ return true;
1109
+ }
1110
+ hasSubscription() {
1111
+ return this.subscriptionId !== null;
1112
+ }
1113
+ isAnnual() {
1114
+ return this.billingCycle === BILLING_CYCLE.YEARLY;
1115
+ }
1116
+ isMonthly() {
1117
+ return this.billingCycle === BILLING_CYCLE.MONTHLY;
1118
+ }
1119
+ isOneOff() {
1120
+ return this.billingCycle === BILLING_CYCLE.ONEOFF || this.billingCycle === null;
1121
+ }
1122
+ getPlanTitle(pricingData) {
1123
+ const plan = pricingData?.plans?.find((p) => isIdsEqual(p.id, this.planId));
1124
+ return plan?.title ?? plan?.name ?? "Deleted Plan";
1125
+ }
1126
+ };
1127
+
1128
+ //#endregion
1129
+ //#region src/services/PurchaseService.ts
1130
+ var PurchaseService = class {
1131
+ constructor(api) {
1132
+ this.api = api;
1133
+ }
1134
+ /**
1135
+ * Retrieve purchase information from the Freemius API based on the license ID.
1136
+ *
1137
+ * The license is the primary entitlement for a purchase, and it may or may not be associated with a subscription.
1138
+ * With this method, you can retrieve detailed information about the purchase, including user details, plan, expiration, and more.
1139
+ */
1140
+ async retrievePurchase(licenseId) {
1141
+ const [license, subscription] = await Promise.all([await this.api.license.retrieve(licenseId), await this.api.license.retrieveSubscription(licenseId)]);
1142
+ if (!license) return null;
1143
+ const user = await this.api.user.retrieve(license.user_id);
1144
+ if (!user) return null;
1145
+ return new PurchaseInfo(user, license, subscription);
1146
+ }
1147
+ /**
1148
+ * A helper method to retrieve raw purchase data instead of a full PurchaseInfo object.
1149
+ *
1150
+ * This is useful when passing data from server to client in frameworks like Next.js, where only serializable data should be sent.
1151
+ */
1152
+ async retrievePurchaseData(licenseId) {
1153
+ const purchaseInfo = await this.retrievePurchase(licenseId);
1154
+ if (!purchaseInfo) return null;
1155
+ return purchaseInfo.toData();
1156
+ }
1157
+ /**
1158
+ * Retrieve a list of active subscriptions for a user. You can use this method to find or sync subscriptions from freemius to your system.
1159
+ */
1160
+ async retrieveSubscriptions(userId, pagination) {
1161
+ const user = await this.api.user.retrieve(userId);
1162
+ if (!user) return [];
1163
+ const subscriptions = await this.api.user.retrieveSubscriptions(userId, { filter: "active" }, pagination);
1164
+ if (!subscriptions || !subscriptions.length) return [];
1165
+ const licenseSubscriptionPromises = subscriptions.map(async (subscription) => {
1166
+ const license = await this.api.license.retrieve(subscription.license_id);
1167
+ if (!license) return null;
1168
+ return new PurchaseInfo(user, license, subscription);
1169
+ });
1170
+ return await Promise.all(licenseSubscriptionPromises).then((results) => results.filter((result) => result !== null).sort((a, b) => b.created.getTime() - a.created.getTime()));
1171
+ }
1172
+ /**
1173
+ * Retrieve a list of purchase data for a user.
1174
+ *
1175
+ * This is a convenience method that returns the purchase data in a format suitable for client-side rendering or serialization.
1176
+ */
1177
+ async retrieveSubscriptionsData(userId, pagination) {
1178
+ const purchaseInfos = await this.retrieveSubscriptions(userId, pagination);
1179
+ return purchaseInfos.map((info) => info.toData());
1180
+ }
1181
+ async retrieveBySubscription(subscription, subscriptionUser) {
1182
+ if (!subscription.license_id) return null;
1183
+ const license = await this.api.license.retrieve(subscription.license_id);
1184
+ if (!license) return null;
1185
+ const user = subscriptionUser && isIdsEqual(subscriptionUser.id, license.user_id) ? subscriptionUser : await this.api.user.retrieve(license.user_id);
1186
+ if (!user) return null;
1187
+ return new PurchaseInfo(user, license, subscription);
1188
+ }
1189
+ async retrieveActiveSubscriptionByEmail(email, pagination) {
1190
+ const user = await this.api.user.retrieveByEmail(email);
1191
+ if (!user) return null;
1192
+ return await this.retrieveSubscriptions(user.id, pagination);
1193
+ }
1194
+ };
1195
+
1196
+ //#endregion
1197
+ //#region src/webhook/WebhookListener.ts
1198
+ const SIGNATURE_HEADER = "x-signature";
1199
+ var WebhookListener = class {
1200
+ eventHandlers = /* @__PURE__ */ new Map();
1201
+ constructor(secretKey, onError = console.error) {
1202
+ this.secretKey = secretKey;
1203
+ this.onError = onError;
1204
+ }
1205
+ on(type, handler) {
1206
+ if (!this.eventHandlers.has(type)) this.eventHandlers.set(type, /* @__PURE__ */ new Set());
1207
+ const existingHandlers = this.eventHandlers.get(type);
1208
+ existingHandlers?.add(handler);
1209
+ return this;
1210
+ }
1211
+ off(type, handler) {
1212
+ const currentHandlers = this.eventHandlers.get(type);
1213
+ if (!currentHandlers) return this;
1214
+ currentHandlers.delete(handler);
1215
+ if (currentHandlers.size === 0) this.eventHandlers.delete(type);
1216
+ return this;
1217
+ }
1218
+ onMultiple(handlers) {
1219
+ for (const [type, handler] of Object.entries(handlers)) if (handler) this.on(type, handler);
1220
+ return this;
1221
+ }
1222
+ removeAll(type) {
1223
+ this.eventHandlers.delete(type);
1224
+ return this;
1225
+ }
1226
+ getHandlerCount(type) {
1227
+ return this.eventHandlers.get(type)?.size ?? 0;
1228
+ }
1229
+ getEventTypeCount() {
1230
+ return this.eventHandlers.size;
1231
+ }
1232
+ getRegisteredEventTypes() {
1233
+ return Array.from(this.eventHandlers.keys());
1234
+ }
1235
+ hasHandlers(type) {
1236
+ const handlers = this.eventHandlers.get(type);
1237
+ return handlers !== void 0 && handlers.size > 0;
1238
+ }
1239
+ hasHandler(type, handler) {
1240
+ const handlers = this.eventHandlers.get(type);
1241
+ return handlers ? handlers.has(handler) : false;
1242
+ }
1243
+ getHandlers(type) {
1244
+ return this.eventHandlers.get(type) || /* @__PURE__ */ new Set();
1245
+ }
1246
+ getTotalHandlerCount() {
1247
+ let total = 0;
1248
+ for (const handlers of this.eventHandlers.values()) total += handlers.size;
1249
+ return total;
1250
+ }
1251
+ /**
1252
+ * Verify hex HMAC signature against the raw body.
1253
+ */
1254
+ verifySignature(rawBody, signature) {
1255
+ if (!signature) return false;
1256
+ const mac = crypto.createHmac("sha256", this.secretKey).update(rawBody).digest("hex");
1257
+ try {
1258
+ return crypto.timingSafeEqual(Buffer.from(mac, "hex"), Buffer.from(signature, "hex"));
1259
+ } catch {
1260
+ return false;
1261
+ }
1262
+ }
1263
+ /**
1264
+ * Process a normalized request.
1265
+ * Returns an object you can map to your framework's response easily.
1266
+ */
1267
+ async process(input) {
1268
+ const sig = this.getHeader(SIGNATURE_HEADER, input.headers);
1269
+ if (!this.verifySignature(input.rawBody, sig)) return {
1270
+ status: 401,
1271
+ success: false,
1272
+ error: "Invalid signature"
1273
+ };
1274
+ let evt;
1275
+ try {
1276
+ const parsed = JSON.parse(typeof input.rawBody === "string" ? input.rawBody : input.rawBody.toString("utf8"));
1277
+ if (!parsed || typeof parsed.type !== "string") return {
1278
+ status: 400,
1279
+ success: false,
1280
+ error: "Invalid payload"
1281
+ };
1282
+ evt = parsed;
1283
+ } catch {
1284
+ return {
1285
+ status: 400,
1286
+ success: false,
1287
+ error: "Malformed JSON"
1288
+ };
1289
+ }
1290
+ const eventType = evt.type;
1291
+ const eventHandlers = this.eventHandlers.get(eventType);
1292
+ if (!eventHandlers || eventHandlers.size === 0) console.warn(`No handlers registered for event type: ${eventType}`);
1293
+ try {
1294
+ const promises = Array.from(eventHandlers || []).map((handler) => {
1295
+ const typedHandler = handler;
1296
+ const typedEvent = evt;
1297
+ return typedHandler(typedEvent);
1298
+ });
1299
+ await Promise.all(promises);
1300
+ } catch (error) {
1301
+ this.onError?.(error);
1302
+ return {
1303
+ status: 500,
1304
+ success: false,
1305
+ error: "Internal Server Error"
1306
+ };
1307
+ }
1308
+ return {
1309
+ status: 200,
1310
+ success: true
1311
+ };
1312
+ }
1313
+ getHeader(name, headers) {
1314
+ const lname = name.toLowerCase();
1315
+ if (headers instanceof Headers) return headers.get(lname);
1316
+ const v = headers[lname] ?? headers[name];
1317
+ if (Array.isArray(v)) return v[0] ?? null;
1318
+ return v ?? null;
1319
+ }
1320
+ };
1321
+
1322
+ //#endregion
1323
+ //#region src/services/WebhookService.ts
1324
+ var WebhookService = class {
1325
+ constructor(secretKey) {
1326
+ this.secretKey = secretKey;
1327
+ }
1328
+ createListener(onError) {
1329
+ return new WebhookListener(this.secretKey, onError);
1330
+ }
1331
+ /**
1332
+ * WHATWG Fetch API adapter for modern JavaScript environments.
1333
+ *
1334
+ * Compatible with:
1335
+ * - Next.js App Router (route.ts files)
1336
+ * - Cloudflare Workers
1337
+ * - Deno
1338
+ * - Bun
1339
+ * - Vercel Edge Functions
1340
+ * - Any environment supporting the WHATWG Fetch API
1341
+ *
1342
+ * This method reads the request body as text and processes the webhook,
1343
+ * returning a standard Response object that can be directly returned
1344
+ * from your endpoint handler.
1345
+ *
1346
+ * @param listener - The webhook listener instance
1347
+ * @param request - The incoming Request object (WHATWG Fetch API)
1348
+ * @returns A Response object with the webhook processing result
1349
+ *
1350
+ * @example
1351
+ * ```typescript
1352
+ * // Next.js App Router (app/webhook/route.ts)
1353
+ * export async function POST(request: Request) {
1354
+ * const listener = webhookService.createListener();
1355
+ * return await webhookService.processFetch(listener, request);
1356
+ * }
1357
+ *
1358
+ * // Cloudflare Workers
1359
+ * export default {
1360
+ * async fetch(request: Request): Promise<Response> {
1361
+ * if (new URL(request.url).pathname === '/webhook') {
1362
+ * const listener = webhookService.createListener();
1363
+ * return await webhookService.processFetch(listener, request);
1364
+ * }
1365
+ * return new Response('Not Found', { status: 404 });
1366
+ * }
1367
+ * };
1368
+ * ```
1369
+ */
1370
+ async processFetch(listener, request) {
1371
+ const rawBody = await request.text();
1372
+ const result = await listener.process({
1373
+ headers: request.headers,
1374
+ rawBody
1375
+ });
1376
+ return new Response(JSON.stringify(result), {
1377
+ status: result.status,
1378
+ headers: { "Content-Type": "application/json" }
1379
+ });
1380
+ }
1381
+ /**
1382
+ * Native Node.js HTTP server adapter.
1383
+ *
1384
+ * Reads the raw body from the request stream and writes the HTTP response directly.
1385
+ *
1386
+ * @example
1387
+ * ```typescript
1388
+ * import { createServer } from 'http';
1389
+ *
1390
+ * const server = createServer(async (req, res) => {
1391
+ * if (req.url === '/webhook') {
1392
+ * await freemius.webhook.processNodeHttp(listener, req, res);
1393
+ * }
1394
+ * });
1395
+ * ```
1396
+ */
1397
+ async processNodeHttp(listener, req, res) {
1398
+ const chunks = [];
1399
+ for await (const chunk of req) chunks.push(chunk);
1400
+ const rawBody = Buffer.concat(chunks);
1401
+ const result = await listener.process({
1402
+ headers: req.headers,
1403
+ rawBody
1404
+ });
1405
+ res.statusCode = result.status;
1406
+ res.setHeader("Content-Type", "application/json");
1407
+ res.end(JSON.stringify(result));
1408
+ }
1409
+ };
1410
+
1411
+ //#endregion
1412
+ //#region src/Freemius.ts
1413
+ var Freemius = class {
1414
+ api;
1415
+ checkout;
1416
+ purchase;
1417
+ customerPortal;
1418
+ webhook;
1419
+ constructor(productId, apiKey, secretKey, publicKey) {
1420
+ this.api = new ApiService(productId, apiKey, secretKey, publicKey);
1421
+ this.checkout = new CheckoutService(productId, publicKey, secretKey);
1422
+ this.purchase = new PurchaseService(this.api);
1423
+ this.customerPortal = new CustomerPortalService(this.api, this.checkout);
1424
+ this.webhook = new WebhookService(secretKey);
1425
+ }
1426
+ };
1427
+
1428
+ //#endregion
1429
+ export { BILLING_CYCLE, CURRENCY, Freemius, PurchaseInfo, idToNumber, idToString, isIdsEqual, parseBillingCycle, parseCurrency, parseDate, parseDateTime, parseNumber, parsePaymentMethod };
1430
+ //# sourceMappingURL=index.mjs.map