Skip to content

Commit 1e96caa

Browse files
committed
[checkout] [new] Add method to create upgrade link or config
Also make the sandbox setter method consistent.
1 parent fb80daf commit 1e96caa

File tree

6 files changed

+87
-28
lines changed

6 files changed

+87
-28
lines changed

.changeset/slick-symbols-act.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@freemius/sdk': minor
3+
---
4+
5+
New feature to generate checkout upgrade flow

packages/sdk/src/Freemius.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class Freemius {
3939
this.auth = new AuthService(productId, secretKey);
4040
this.pricing = new PricingService(this.api);
4141
this.purchase = new PurchaseService(this.api);
42-
this.checkout = new CheckoutService(productId, publicKey, secretKey, this.purchase, this.pricing);
42+
this.checkout = new CheckoutService(this.api, productId, publicKey, secretKey, this.purchase, this.pricing);
4343
this.customerPortal = new CustomerPortalService(this.api, this.checkout, this.auth, this.purchase);
4444
this.webhook = new WebhookService(this.api, secretKey);
4545
}

packages/sdk/src/__tests__/manual/checkout.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { freemius } from './fs';
22

33
async function main() {
4-
const checkoutLink = (await freemius.checkout.create()).setSandbox().getLink();
4+
const checkoutLink = (await freemius.checkout.create())
5+
.setSandbox(await freemius.checkout.getSandboxParams())
6+
.getLink();
57
console.log('Checkout Link:', checkoutLink);
68

7-
const checkoutOptions = (await freemius.checkout.create()).setSandbox().getOptions();
9+
const checkoutOptions = (await freemius.checkout.create())
10+
.setSandbox(await freemius.checkout.getSandboxParams())
11+
.getOptions();
812
console.log('Checkout Options:', checkoutOptions);
913

1014
const checkout = await freemius.checkout.create({
@@ -18,6 +22,12 @@ async function main() {
1822

1923
const pricingData = await freemius.pricing.retrieve();
2024
console.log('Pricing Data:', pricingData);
25+
26+
const checkoutUpgrader = (await freemius.checkout.create()).setLicenseAuthorization(
27+
await freemius.checkout.getLicenseUpgradeParams('6')
28+
);
29+
checkoutUpgrader.setPlan('383');
30+
console.log('License Upgrade Link:', checkoutUpgrader.getLink());
2131
}
2232

2333
main();

packages/sdk/src/checkout/Checkout.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ import {
44
convertCheckoutOptionsToQueryParams,
55
buildFreemiusQueryFromOptions,
66
} from '@freemius/checkout';
7-
import { createHash } from 'crypto';
87
import { isTestServer, splitName } from '../utils/ops';
98
import { CheckoutBuilderUserOptions } from '../contracts/checkout';
9+
import { FSId } from '../api/types';
1010

1111
export type CheckoutSerialized = {
1212
options: CheckoutOptions;
1313
link: string;
1414
baseUrl: string;
1515
};
1616

17+
export type CheckoutLicenseAuthorization = { licenseId: FSId; authorization: string };
18+
1719
/**
1820
* A builder class for constructing checkout parameters. This class provides a fluent
1921
* API to create Checkout parameters for a product with various configurations.
@@ -22,20 +24,6 @@ export type CheckoutSerialized = {
2224
* The final `getOptions()` method returns the constructed `CheckoutOptions` object.
2325
*/
2426
export class Checkout {
25-
static createSandboxToken(
26-
productId: string,
27-
secretKey: string,
28-
publicKey: string
29-
): NonNullable<CheckoutPopupParams['sandbox']> {
30-
const timestamp = Math.floor(Date.now() / 1000).toString();
31-
const token = `${timestamp}${productId}${secretKey}${publicKey}checkout`;
32-
33-
return {
34-
ctx: timestamp,
35-
token: createHash('md5').update(token).digest('hex'),
36-
};
37-
}
38-
3927
private options: CheckoutOptions;
4028

4129
constructor(
@@ -51,10 +39,10 @@ export class Checkout {
5139
*
5240
* @returns A new builder instance with sandbox configuration
5341
*/
54-
setSandbox(): Checkout {
42+
setSandbox(sandbox: NonNullable<CheckoutPopupParams['sandbox']>): Checkout {
5543
this.options = {
5644
...this.options,
57-
sandbox: Checkout.createSandboxToken(this.productId, this.secretKey!, this.publicKey!),
45+
sandbox,
5846
};
5947

6048
return this;
@@ -406,7 +394,7 @@ export class Checkout {
406394
* @param licenseKey The license key to renew
407395
* @returns A new builder instance configured for renewal
408396
*/
409-
setLicenseRenewal(licenseKey: string): Checkout {
397+
setLicenseRenewalByKey(licenseKey: string): Checkout {
410398
this.options = {
411399
...this.options,
412400
license_key: licenseKey,
@@ -415,6 +403,16 @@ export class Checkout {
415403
return this;
416404
}
417405

406+
setLicenseAuthorization(params: CheckoutLicenseAuthorization): Checkout {
407+
this.options = {
408+
...this.options,
409+
license_id: params.licenseId,
410+
authorization: params.authorization,
411+
};
412+
413+
return this;
414+
}
415+
418416
/**
419417
* Builds and returns the final checkout options to be used with the `@freemius/checkout` package.
420418
*

packages/sdk/src/contracts/checkout.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ export type CheckoutBuilderOptions = {
145145
* If not provided, the checkout will not have a trial period.
146146
*/
147147
trial?: CheckoutOptions['trial'];
148+
/**
149+
* Optional license ID for license upgrade scenarios.
150+
*
151+
* When provided, the checkout will be configured for upgrading the specified license. Use this to generate upgrade links for existing users.
152+
*/
153+
licenseId?: string;
148154
};
149155

150156
/**

packages/sdk/src/services/CheckoutService.ts

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { CheckoutPopupParams } from '@freemius/checkout';
22
import { FSId } from '../api/types';
33
import { idToString } from '../api/parser';
4-
import { Checkout } from '../checkout/Checkout';
4+
import { Checkout, CheckoutLicenseAuthorization } from '../checkout/Checkout';
55
import { CheckoutBuilderOptions } from '../contracts/checkout';
66
import { PurchaseService } from './PurchaseService';
77
import { PricingService } from './PricingService';
88
import { CheckoutRequestProcessor } from '../checkout/CheckoutRequestProcessor';
99
import { RedirectProcessor } from '../checkout/RedirectProcessor';
10+
import { createHash } from 'crypto';
11+
import { ApiService } from './ApiService';
1012

1113
export class CheckoutService {
1214
public readonly request: CheckoutRequestProcessor;
1315

1416
constructor(
17+
private readonly api: ApiService,
1518
private readonly productId: FSId,
1619
private readonly publicKey: string,
1720
private readonly secretKey: string,
@@ -54,7 +57,17 @@ export class CheckoutService {
5457
* @example
5558
*/
5659
async create(options: CheckoutBuilderOptions = {}): Promise<Checkout> {
57-
const { user, isSandbox = false, withRecommendation = true, title, image, planId, quota, trial } = options;
60+
const {
61+
user,
62+
isSandbox = false,
63+
withRecommendation = true,
64+
title,
65+
image,
66+
planId,
67+
quota,
68+
trial,
69+
licenseId,
70+
} = options;
5871

5972
const builder = new Checkout(idToString(this.productId), this.publicKey, this.secretKey);
6073

@@ -67,7 +80,7 @@ export class CheckoutService {
6780
}
6881

6982
if (isSandbox) {
70-
builder.setSandbox();
83+
builder.setSandbox(await this.getSandboxParams());
7184
}
7285

7386
if (title) {
@@ -90,6 +103,11 @@ export class CheckoutService {
90103
builder.setTrial(trial);
91104
}
92105

106+
if (licenseId) {
107+
const authorization = await this.getLicenseUpgradeParams(licenseId);
108+
builder.setLicenseAuthorization(authorization);
109+
}
110+
93111
return builder;
94112
}
95113

@@ -99,12 +117,34 @@ export class CheckoutService {
99117
* This shouldn't be used in production, but is useful for testing purposes.
100118
*
101119
* @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).
102-
*
103-
* @todo - This has a duplication with the `inSandbox` method in the builder. Consider refactoring to avoid this duplication.
104-
* Also think about whether we should make the builder's `inSandbox` method async as well.
105120
*/
106121
async getSandboxParams(): Promise<NonNullable<CheckoutPopupParams['sandbox']>> {
107-
return Checkout.createSandboxToken(idToString(this.productId), this.secretKey, this.publicKey);
122+
const productId = idToString(this.productId);
123+
const timestamp = Math.floor(Date.now() / 1000).toString();
124+
const token = `${timestamp}${productId}${this.secretKey}${this.publicKey}checkout`;
125+
126+
return {
127+
ctx: timestamp,
128+
token: createHash('md5').update(token).digest('hex'),
129+
};
130+
}
131+
132+
/**
133+
* Retrieves the license upgrade authorization for a given license ID.
134+
*
135+
* This is used to authorize a license upgrade during the checkout process. Useful when creating upgrade links for existing users.
136+
*/
137+
async getLicenseUpgradeParams(licenseId: FSId): Promise<CheckoutLicenseAuthorization> {
138+
const auth = await this.api.license.retrieveCheckoutUpgradeAuthorization(licenseId);
139+
140+
if (!auth) {
141+
throw new Error('Failed to retrieve license upgrade authorization');
142+
}
143+
144+
return {
145+
licenseId,
146+
authorization: auth,
147+
};
108148
}
109149

110150
/**

0 commit comments

Comments
 (0)