@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.d.mts +14037 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +14037 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1463 -0
- package/dist/index.mjs +1430 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
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;
|