chargebee-init 1.0.0-beta.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.
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../dist/cli.js";
4
+
5
+ function onError(err) {
6
+ console.error(err);
7
+ process.exit(1);
8
+ }
9
+
10
+ process.on("uncaughtException", onError);
11
+
12
+ run().catch(onError);
@@ -0,0 +1,17 @@
1
+ import type { PackageManifest } from "@pnpm/types";
2
+ import { type DetectedFramework } from "./frameworks.js";
3
+ export type Checks = "git" | "package.json" | "framework";
4
+ export type CheckError = {
5
+ check: Checks;
6
+ msg: string;
7
+ bail?: boolean;
8
+ };
9
+ export type CheckResponse<P extends string, T> = {
10
+ [k in P]?: T;
11
+ } & {
12
+ errors: CheckError[];
13
+ };
14
+ export type PreflightResponse = CheckResponse<"pkg", PackageManifest>;
15
+ export type FrameworkResponse = CheckResponse<"framework", DetectedFramework>;
16
+ export declare const preflightChecks: (path: string) => Promise<PreflightResponse>;
17
+ export declare const frameworkChecks: (pkg: PackageManifest) => Promise<FrameworkResponse>;
package/dist/checks.js ADDED
@@ -0,0 +1,46 @@
1
+ import colors from "ansi-colors";
2
+ import { detectFramework, satisfiesMinVersion, supportedFrameworks, } from "./frameworks.js";
3
+ import { isCleanTree } from "./git.js";
4
+ import { getPackageJson } from "./package.js";
5
+ export const preflightChecks = async (path) => {
6
+ const errors = [];
7
+ // We raise a warning if we can't validate that the target directory isn't
8
+ // a git tree, but give them the option to continue
9
+ const cleanTree = await isCleanTree(path);
10
+ if (!cleanTree) {
11
+ errors.push({
12
+ check: "git",
13
+ msg: `Could not validate if the target directory is a clean git tree`,
14
+ });
15
+ }
16
+ // This is a hard stop as we can't continue without a valid package.json
17
+ const pkg = getPackageJson(path);
18
+ if (pkg) {
19
+ return { pkg, errors };
20
+ }
21
+ errors.push({
22
+ check: "package.json",
23
+ msg: `Could not read the contents of ${path}/package.json to perform the required checks. Please ensure your app has a valid package.json.`,
24
+ bail: true,
25
+ });
26
+ return { errors };
27
+ };
28
+ export const frameworkChecks = async (pkg) => {
29
+ const errors = [];
30
+ const framework = detectFramework(pkg);
31
+ if (!framework) {
32
+ errors.push({
33
+ check: "framework",
34
+ msg: `No supported framework detected in package.json. We currently support: ${colors.red(Object.keys(supportedFrameworks).join(", "))}`,
35
+ bail: true,
36
+ });
37
+ }
38
+ else if (!satisfiesMinVersion(framework)) {
39
+ errors.push({
40
+ check: "framework",
41
+ msg: `${framework.name}@${framework.version} does not satisfy the minimum version we support (${framework.info.minVersion}). Can't proceed!`,
42
+ bail: true,
43
+ });
44
+ }
45
+ return { framework, errors };
46
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function run(): Promise<void>;
package/dist/cli.js ADDED
@@ -0,0 +1,44 @@
1
+ import colors from "ansi-colors";
2
+ import meow from "meow";
3
+ import * as help from "./help.js";
4
+ import { init } from "./init.js";
5
+ const cli = meow(`
6
+ Usage
7
+ $ chargebee-init <command> [subcommand]
8
+
9
+ Options
10
+ --help display this help text
11
+
12
+ Examples
13
+ $ chargebee-init
14
+ $ chargebee-init help
15
+ $ chargebee-init help nextjs|express
16
+ `, {
17
+ importMeta: import.meta,
18
+ autoHelp: true,
19
+ autoVersion: true,
20
+ });
21
+ export async function run() {
22
+ const [command = "", subcommand = ""] = cli.input;
23
+ switch (command) {
24
+ case "":
25
+ case "init":
26
+ await init();
27
+ break;
28
+ case "help":
29
+ {
30
+ if (!subcommand || !(subcommand in help.messages)) {
31
+ console.log(colors.cyanBright(help.cliHelpMsg(cli.pkg.version, cli.pkg.description)));
32
+ console.log(cli.help.replace(cli.pkg.description, "").trim());
33
+ }
34
+ else {
35
+ const framework = subcommand;
36
+ console.log(help.messages[framework].preinit);
37
+ console.log(colors.yellowBright(help.messages[framework].postinit));
38
+ }
39
+ }
40
+ break;
41
+ default:
42
+ cli.showHelp();
43
+ }
44
+ }
@@ -0,0 +1,17 @@
1
+ import type { PackageManifest } from "@pnpm/types";
2
+ export type Framework = "nextjs" | "express";
3
+ export type Features = "checkout" | "webhook";
4
+ export interface FrameworkInfo {
5
+ packageName: string;
6
+ minVersion: string;
7
+ dependencies: string[];
8
+ appDirectories: string[];
9
+ }
10
+ export declare const supportedFrameworks: Record<Framework, FrameworkInfo>;
11
+ export type DetectedFramework = {
12
+ name: Framework;
13
+ version: string;
14
+ info: FrameworkInfo;
15
+ };
16
+ export declare function detectFramework(pkgJson: PackageManifest): DetectedFramework | undefined;
17
+ export declare function satisfiesMinVersion({ version, info, }: DetectedFramework): boolean;
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import semver from "semver";
3
+ export const supportedFrameworks = {
4
+ nextjs: {
5
+ packageName: "next",
6
+ minVersion: "15",
7
+ dependencies: ["@chargebee/nextjs:~0.1.0"],
8
+ appDirectories: ["app", path.join("src", "app")],
9
+ },
10
+ express: {
11
+ packageName: "express",
12
+ minVersion: ">=5",
13
+ dependencies: ["@chargebee/express:~0.1.0"],
14
+ appDirectories: ["app", "src"],
15
+ },
16
+ };
17
+ // We read the version from package.json, which will not
18
+ // give us the exact installed version thanks to semver. But as
19
+ // long as it satisfies our minVersion, that should be ok.
20
+ //
21
+ // TODO: What if there are multiple detected frameworks?
22
+ export function detectFramework(pkgJson) {
23
+ if (!pkgJson.dependencies) {
24
+ return undefined;
25
+ }
26
+ for (const [framework, info] of Object.entries(supportedFrameworks)) {
27
+ const version = pkgJson.dependencies[info.packageName];
28
+ if (version) {
29
+ return {
30
+ name: framework,
31
+ version,
32
+ info,
33
+ };
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+ export function satisfiesMinVersion({ version, info, }) {
39
+ var _a, _b;
40
+ const ver = (_b = (_a = semver.coerce(version)) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : "";
41
+ return semver.satisfies(ver, info.minVersion);
42
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function isCleanTree(dir: string): Promise<boolean>;
package/dist/git.js ADDED
@@ -0,0 +1,10 @@
1
+ import { simpleGit } from "simple-git";
2
+ export async function isCleanTree(dir) {
3
+ try {
4
+ const git = simpleGit(dir);
5
+ return (await git.status()).isClean();
6
+ }
7
+ catch (_err) {
8
+ return false;
9
+ }
10
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export declare const cliHelpMsg: (version: string, description: string) => string;
2
+ export declare const messages: {
3
+ readonly nextjs: {
4
+ readonly preinit: "\n* --------------------------\n* Chargebee Next.js Adapter\n* --------------------------\n* Integrates with Next.js version 15 \n* Only App Router is supported at the moment\n* Routes will be created under the chargebee directory by default\n";
5
+ readonly postinit: "\nPlease complete the following steps before you test out the Chargebee integration:\n\n* Define the required process.env.* variables either by adding them to your .env file or replacing them at build time:\n\n\tCHARGEBEE_SITE=\"site-name\"\n\tCHARGEBEE_API_KEY=\"\"\n\tCHARGEBEE_WEBHOOK_AUTH=\"username:password\"\n\n* Run npm|pnpm|bun install to grab the required packages\n\n* Configure the webhook URL in the Chargebee dashboard with basic auth set to the username and password defined in CHARGEBEE_WEBHOOK_AUTH\n\n* Review the routes created under the chargebee/ directory and make necessary changes\n";
6
+ };
7
+ readonly express: {
8
+ readonly preinit: "\n* --------------------------\n* Chargebee Express Adapter\n* --------------------------\n* Integrates with Express version 5\n";
9
+ readonly postinit: "\nPlease complete the following steps before you test out the Chargebee integration:\n\n* Define the required process.env.* variables either by adding them to your .env file or replacing them at build time:\n\n\tCHARGEBEE_SITE=\"site-name\"\n\tCHARGEBEE_API_KEY=\"\"\n\tCHARGEBEE_WEBHOOK_AUTH=\"username:password\"\n\n* Run npm|pnpm|bun install to grab the required packages\n\n* Configure the webhook URL in the Chargebee dashboard with basic auth set to the username and password defined in CHARGEBEE_WEBHOOK_AUTH\n\n* Review chargebee/controllers.ts and make necessary changes\n\n* Wire up the routes in your express app:\n\n\timport chargebeeInit from \"./chargebee/controllers.ts\"\n\tchargebeeInit(app);\n";
10
+ };
11
+ };
package/dist/help.js ADDED
@@ -0,0 +1,58 @@
1
+ export const cliHelpMsg = (version, description) => `
2
+ chargebee-init v${version}
3
+
4
+ ${description}. Supports these popular frameworks:
5
+
6
+ Next.js v15
7
+ Express v5
8
+
9
+ And these features using the Chargebee Node SDK:
10
+
11
+ Checkout (Hosted Pages): One time, subscription and manage payment method
12
+ Webhook: Incoming webhooks
13
+ `;
14
+ const commonPostInit = `Please complete the following steps before you test out the Chargebee integration:
15
+
16
+ * Define the required process.env.* variables either by adding them to your .env file or replacing them at build time:
17
+
18
+ CHARGEBEE_SITE="site-name"
19
+ CHARGEBEE_API_KEY=""
20
+ CHARGEBEE_WEBHOOK_AUTH="username:password"
21
+
22
+ * Run npm|pnpm|bun install to grab the required packages
23
+
24
+ * Configure the webhook URL in the Chargebee dashboard with basic auth set to the username and password defined in CHARGEBEE_WEBHOOK_AUTH
25
+ `;
26
+ export const messages = {
27
+ nextjs: {
28
+ preinit: `
29
+ * --------------------------
30
+ * Chargebee Next.js Adapter
31
+ * --------------------------
32
+ * Integrates with Next.js version 15
33
+ * Only App Router is supported at the moment
34
+ * Routes will be created under the chargebee directory by default
35
+ `,
36
+ postinit: `
37
+ ${commonPostInit}
38
+ * Review the routes created under the chargebee/ directory and make necessary changes
39
+ `,
40
+ },
41
+ express: {
42
+ preinit: `
43
+ * --------------------------
44
+ * Chargebee Express Adapter
45
+ * --------------------------
46
+ * Integrates with Express version 5
47
+ `,
48
+ postinit: `
49
+ ${commonPostInit}
50
+ * Review chargebee/controllers.ts and make necessary changes
51
+
52
+ * Wire up the routes in your express app:
53
+
54
+ import chargebeeInit from "./chargebee/controllers.ts"
55
+ chargebeeInit(app);
56
+ `,
57
+ },
58
+ };
package/dist/init.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare const init: () => Promise<void>;
package/dist/init.js ADDED
@@ -0,0 +1,66 @@
1
+ import colors from "ansi-colors";
2
+ import Enquirer from "enquirer";
3
+ import { frameworkChecks, preflightChecks } from "./checks.js";
4
+ import { supportedFrameworks } from "./frameworks.js";
5
+ import * as help from "./help.js";
6
+ import { updateDependencies, writePackageJson } from "./package.js";
7
+ import { confirmWritePrompt, gitPrompt, pathPrefixPrompt, targetDirPrompt, } from "./prompts.js";
8
+ import { copyTemplates } from "./templates.js";
9
+ const error = (...lines) => {
10
+ console.error(`\n${lines.map((line) => colors.red(`✖ ${line}`)).join("\n")}\n`);
11
+ process.exit(1);
12
+ };
13
+ const checkErrors = ({ errors }) => {
14
+ const bailErrors = errors.filter((err) => err.bail);
15
+ if (bailErrors.length > 0) {
16
+ error(...bailErrors.map((err) => err.msg));
17
+ }
18
+ };
19
+ export const init = async () => {
20
+ const cwd = process.cwd();
21
+ const enquirer = new Enquirer();
22
+ const { targetDir } = (await enquirer.prompt(targetDirPrompt(cwd)));
23
+ // General checks
24
+ const preflightResponse = await preflightChecks(targetDir);
25
+ checkErrors(preflightResponse);
26
+ if (preflightResponse.errors.length > 0) {
27
+ const { gitConfirm } = (await enquirer.prompt(gitPrompt(preflightResponse)));
28
+ if (!gitConfirm) {
29
+ error("Did not make any changes");
30
+ }
31
+ }
32
+ // Check target framework and version
33
+ // biome-ignore lint/style/noNonNullAssertion: pkg will always be available here
34
+ const pkg = preflightResponse.pkg;
35
+ const frameworkResponse = await frameworkChecks(pkg);
36
+ checkErrors(frameworkResponse);
37
+ if (!frameworkResponse) {
38
+ throw new Error(`Could not determine framework in package`);
39
+ }
40
+ const { pathPrefix } = (await enquirer.prompt(pathPrefixPrompt()));
41
+ // biome-ignore lint/style/noNonNullAssertion: framework will always be available here
42
+ const detectedFramework = frameworkResponse.framework;
43
+ const { confirmWrite } = (await enquirer.prompt(confirmWritePrompt(detectedFramework)));
44
+ if (!confirmWrite) {
45
+ console.log(colors.yellow("Not proceeding, did not make any changes"));
46
+ process.exit(0);
47
+ }
48
+ // Copy templates
49
+ try {
50
+ const frameworkName = detectedFramework.name;
51
+ const updatedFiles = copyTemplates({
52
+ targetDir,
53
+ framework: frameworkName,
54
+ frameworkInfo: supportedFrameworks[frameworkName],
55
+ pathPrefix,
56
+ });
57
+ const updatedPkg = updateDependencies(pkg, detectedFramework);
58
+ writePackageJson(targetDir, updatedPkg);
59
+ updatedFiles.push(`package.json`);
60
+ console.log(colors.green(`\nThe following files were created or updated: \n${updatedFiles.join("\n")}\n`));
61
+ console.log(colors.yellow(help.messages[frameworkName].postinit));
62
+ }
63
+ catch (err) {
64
+ error("Could not copy files to the app directory", err);
65
+ }
66
+ };
@@ -0,0 +1,5 @@
1
+ import type { PackageManifest } from "@pnpm/types";
2
+ import type { DetectedFramework } from "./frameworks.js";
3
+ export declare function getPackageJson(dir: string): PackageManifest | undefined;
4
+ export declare function updateDependencies(pkg: PackageManifest, framework: DetectedFramework): PackageManifest;
5
+ export declare function writePackageJson(dir: string, pkg: PackageManifest): void;
@@ -0,0 +1,31 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ export function getPackageJson(dir) {
4
+ try {
5
+ const pkgJson = fs.readFileSync(path.join(dir, "package.json"), {
6
+ encoding: "utf8",
7
+ });
8
+ return typeof pkgJson === "string"
9
+ ? JSON.parse(pkgJson)
10
+ : undefined;
11
+ }
12
+ catch (_err) {
13
+ return undefined;
14
+ }
15
+ }
16
+ export function updateDependencies(pkg, framework) {
17
+ var _a;
18
+ (_a = pkg.dependencies) !== null && _a !== void 0 ? _a : (pkg.dependencies = {});
19
+ framework.info.dependencies.forEach((dep) => {
20
+ const [name, version] = dep.split(":");
21
+ if (name && version && pkg.dependencies) {
22
+ pkg.dependencies[name] = version;
23
+ }
24
+ });
25
+ return pkg;
26
+ }
27
+ export function writePackageJson(dir, pkg) {
28
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkg, null, 2), {
29
+ encoding: "utf8",
30
+ });
31
+ }
@@ -0,0 +1,10 @@
1
+ import type Enquirer from "enquirer";
2
+ import type { PreflightResponse } from "./checks.js";
3
+ import type { DetectedFramework } from "./frameworks.js";
4
+ type Prompt = Parameters<typeof Enquirer.prompt>[0];
5
+ export declare const targetDirPrompt: (cwd: string) => Prompt;
6
+ export declare const gitPrompt: (preflight: PreflightResponse) => Prompt;
7
+ export declare const confirmWritePrompt: (framework: DetectedFramework) => Prompt;
8
+ export declare const apiAuthPrompt: () => Prompt;
9
+ export declare const pathPrefixPrompt: () => Prompt;
10
+ export {};
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import colors from "ansi-colors";
3
+ import * as help from "./help.js";
4
+ export const targetDirPrompt = (cwd) => ({
5
+ type: "text",
6
+ name: "targetDir",
7
+ message: `Path to your existing app`,
8
+ initial: cwd,
9
+ validate(path) {
10
+ if (!(fs.existsSync(path) && fs.lstatSync(path).isDirectory())) {
11
+ return `${path} doesn't exist or is not a directory`;
12
+ }
13
+ return true;
14
+ },
15
+ });
16
+ export const gitPrompt = (preflight) => ({
17
+ type: "confirm",
18
+ name: "gitConfirm",
19
+ message: () => {
20
+ var _a;
21
+ return colors.yellow(`${(_a = preflight.errors.filter((err) => !err.bail)[0]) === null || _a === void 0 ? void 0 : _a.msg}. Do you want to continue?`);
22
+ },
23
+ });
24
+ export const confirmWritePrompt = (framework) => ({
25
+ type: "confirm",
26
+ name: "confirmWrite",
27
+ message: () => `Supported version of ${framework.name} found! Please read these details to continue: \n
28
+ ${colors.cyanBright(help.messages[framework.name].preinit)}
29
+
30
+ The next step is to create the required files and update package.json with the dependencies.
31
+ ${colors.green("Do you want to continue?")}`,
32
+ });
33
+ export const apiAuthPrompt = () => [
34
+ {
35
+ type: "input",
36
+ name: "siteName",
37
+ message: "Site name",
38
+ initial: "site-test",
39
+ },
40
+ {
41
+ type: "password",
42
+ name: "apiKey",
43
+ message() {
44
+ const siteName = this.state.answers.siteName;
45
+ const url = `https://${siteName}.chargebee.com/apikeys_and_webhooks/api`;
46
+ return `API key [${colors.underline(colors.gray(url))}]`;
47
+ },
48
+ },
49
+ ];
50
+ export const pathPrefixPrompt = () => ({
51
+ type: "input",
52
+ name: "pathPrefix",
53
+ message: "The base path prefix for all the routes created. You can edit the generated files to change this later",
54
+ initial: "",
55
+ result(value) {
56
+ return value.trim() ? `/${value.trim().replace(/^\/*|\/*$/g, "")}` : "";
57
+ },
58
+ });
@@ -0,0 +1,184 @@
1
+ import {
2
+ type Chargebee,
3
+ ChargebeeClient,
4
+ type ChargeInput,
5
+ createOneTimeCheckout,
6
+ createPortalSession,
7
+ createSubscriptionCheckout,
8
+ type ManageInput,
9
+ managePaymentSources,
10
+ type PortalCreateInput,
11
+ type SubscriptionInput,
12
+ validateBasicAuth,
13
+ } from "@chargebee/express";
14
+ import type { Application, Request, Response } from "express";
15
+
16
+ const apiKey = process.env.CHARGEBEE_API_KEY!;
17
+ const site = process.env.CHARGEBEE_SITE!;
18
+ const webhookBasicAuth = process.env.CHARGEBEE_WEBHOOK_AUTH;
19
+
20
+ const chargebee = new ChargebeeClient({ apiKey, site });
21
+
22
+ /**
23
+ * Checkout an item without creating a subscription
24
+ */
25
+ const chargeController = createOneTimeCheckout({
26
+ apiKey,
27
+ site,
28
+ apiPayload: async (req: Request) => {
29
+ console.warn(
30
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
31
+ );
32
+ // https://api-explorer.chargebee.com/item_prices/list_item_prices
33
+ const { list } = await chargebee.itemPrice.list({
34
+ item_type: {
35
+ is: "charge",
36
+ },
37
+ status: {
38
+ is: "active",
39
+ },
40
+ });
41
+
42
+ return {
43
+ item_prices: list.map((entry) => ({
44
+ item_price_id: entry.item_price.id,
45
+ })),
46
+ redirect_url: `${req.baseUrl}{{pathPrefix}}/checkout/callback`,
47
+ } as ChargeInput;
48
+ },
49
+ });
50
+
51
+ /**
52
+ * Create a subscription
53
+ */
54
+ const subscriptionController = createSubscriptionCheckout({
55
+ apiKey,
56
+ site,
57
+ apiPayload: async (req: Request) => {
58
+ console.warn(
59
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
60
+ );
61
+ // https://api-explorer.chargebee.com/item_prices/list_item_prices
62
+ const { list } = await chargebee.itemPrice.list({
63
+ limit: 1,
64
+ item_type: {
65
+ is: "plan",
66
+ },
67
+ status: {
68
+ is: "active",
69
+ },
70
+ });
71
+
72
+ return {
73
+ subscription_items: [{ item_price_id: list[0]?.item_price.id }],
74
+ redirect_url: `${req.baseUrl}{{pathPrefix}}/checkout/callback`,
75
+ } as SubscriptionInput;
76
+ },
77
+ });
78
+
79
+ /**
80
+ * Let the customer add/remove payment sources
81
+ */
82
+ const manageController = managePaymentSources({
83
+ apiKey,
84
+ site,
85
+ apiPayload: async (req: Request) => {
86
+ console.warn(
87
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
88
+ );
89
+ // https://apidocs.chargebee.com/docs/api/hosted_pages?lang=node#manage_payment_sources
90
+ return {
91
+ customer: {
92
+ id: "chargebee-customer-id",
93
+ redirect_url: `${req.baseUrl}{{pathPrefix}}/checkout/callback`,
94
+ pass_thru_content: crypto.randomUUID(),
95
+ },
96
+ } as ManageInput;
97
+ },
98
+ });
99
+
100
+ /**
101
+ * Open the Chargebee portal for the given cutomer ID
102
+ */
103
+ const portalController = createPortalSession({
104
+ apiKey,
105
+ site,
106
+ apiPayload: async (req: Request) => {
107
+ console.warn(
108
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
109
+ );
110
+ // TODO: Return the authenticated customer here
111
+ return {
112
+ customer: {
113
+ id: "chargebee-customer-id",
114
+ },
115
+ redirect_url: `${req.baseUrl}/users/`,
116
+ } as PortalCreateInput;
117
+ },
118
+ });
119
+
120
+ /**
121
+ * Checkout callback function
122
+ */
123
+ async function callbackController(req: Request, _res: Response) {
124
+ console.warn(
125
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
126
+ );
127
+ const { searchParams } = new URL(req.originalUrl);
128
+ const id = searchParams.get("id");
129
+ const state = searchParams.get("state");
130
+ // TODO: validate state and do something with the hosted page id
131
+ if (state === "succeeded") {
132
+ const { hosted_page } = await chargebee.hostedPage.retrieve(id!);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Handle incoming webhook events
138
+ */
139
+ async function webhookController(req: Request, _res: Response) {
140
+ console.warn(
141
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
142
+ );
143
+ // HTTP Basic Auth is currently optional when adding a new webhook
144
+ // url in the Chargebee dashboard. However, we expect it's set by default.
145
+ // Please set the env variable CHARGEBEE_WEBHOOK_BASIC_AUTH to "user:pass"
146
+ // which is validated here
147
+ try {
148
+ validateBasicAuth(webhookBasicAuth, req.get("authorization"));
149
+ } catch (error) {
150
+ console.error(error);
151
+ }
152
+
153
+ const data = req.body as Chargebee.Event;
154
+ // TODO: handle the incoming webhook data
155
+ console.log(data);
156
+ }
157
+
158
+ /**
159
+ * Initialize the Express app with the Chargebee controllers
160
+ * @param app Express application
161
+ * @returns Express application
162
+ */
163
+ export default function init(
164
+ app: Application,
165
+ { routePrefix = "{{pathPrefix}}" } = {} as { routePrefix: string },
166
+ ) {
167
+ // Checkout
168
+ app.get(`${routePrefix}/checkout/one-time-charge`, chargeController);
169
+ app.get(`${routePrefix}/checkout/subscription`, subscriptionController);
170
+ app.get(`${routePrefix}/checkout/manage-payment-sources`, manageController);
171
+ app.get(`${routePrefix}/checkout/callback`, callbackController);
172
+
173
+ // Portal
174
+ app.get(`${routePrefix}/portal`, portalController);
175
+
176
+ // Webhook
177
+ app.post(`${routePrefix}/webhook`, webhookController);
178
+
179
+ return app;
180
+ }
181
+
182
+ // Usage:
183
+ // import chargebeeInit from "./chargebee/controllers.ts"
184
+ // chargebeeInit(app);
@@ -0,0 +1,15 @@
1
+ import { client } from "@chargebee/nextjs";
2
+ import type { NextRequest } from "next/server.js";
3
+
4
+ export const GET = async (req: NextRequest) => {
5
+ console.warn(
6
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
7
+ );
8
+ const id = req.nextUrl.searchParams.get("id");
9
+ const state = req.nextUrl.searchParams.get("state");
10
+ // TODO: validate state and do something with the hosted page id
11
+ const chargebee = await client.getFromEnv();
12
+ if (state === "succeeded") {
13
+ const { hosted_page } = await chargebee.hostedPage.retrieve(id!);
14
+ }
15
+ };
@@ -0,0 +1,20 @@
1
+ import { type ManageInput, managePaymentSources } from "@chargebee/nextjs";
2
+ import type { NextRequest } from "next/server.js";
3
+
4
+ export const GET = managePaymentSources({
5
+ apiKey: process.env.CHARGEBEE_API_KEY!,
6
+ site: process.env.CHARGEBEE_SITE!,
7
+ apiPayload: (req: NextRequest): ManageInput => {
8
+ console.warn(
9
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
10
+ );
11
+ // https://apidocs.chargebee.com/docs/api/hosted_pages?lang=node#manage_payment_sources
12
+ return {
13
+ customer: {
14
+ id: "chargebee-customer-id",
15
+ redirect_url: `${req.nextUrl.origin}{{pathPrefix}}/checkout/callback`,
16
+ pass_thru_content: crypto.randomUUID(),
17
+ },
18
+ } as ManageInput;
19
+ },
20
+ });
@@ -0,0 +1,33 @@
1
+ import {
2
+ type ChargeInput,
3
+ client,
4
+ createOneTimeCheckout,
5
+ } from "@chargebee/nextjs";
6
+ import type { NextRequest } from "next/server.js";
7
+
8
+ export const GET = createOneTimeCheckout({
9
+ apiKey: process.env.CHARGEBEE_API_KEY!,
10
+ site: process.env.CHARGEBEE_SITE!,
11
+ apiPayload: async (req: NextRequest): Promise<ChargeInput> => {
12
+ console.warn(
13
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
14
+ );
15
+ const chargebee = await client.getFromEnv();
16
+ // https://api-explorer.chargebee.com/item_prices/list_item_prices
17
+ const { list } = await chargebee.itemPrice.list({
18
+ item_type: {
19
+ is: "charge",
20
+ },
21
+ status: {
22
+ is: "active",
23
+ },
24
+ });
25
+
26
+ return {
27
+ item_prices: list.map((entry) => ({
28
+ item_price_id: entry.item_price.id,
29
+ })),
30
+ redirect_url: `${req.nextUrl.origin}{{pathPrefix}}/checkout/callback`,
31
+ } as ChargeInput;
32
+ },
33
+ });
@@ -0,0 +1,32 @@
1
+ import {
2
+ client,
3
+ createSubscriptionCheckout,
4
+ type SubscriptionInput,
5
+ } from "@chargebee/nextjs";
6
+ import type { NextRequest } from "next/server.js";
7
+
8
+ export const GET = createSubscriptionCheckout({
9
+ apiKey: process.env.CHARGEBEE_API_KEY!,
10
+ site: process.env.CHARGEBEE_SITE!,
11
+ apiPayload: async (req: NextRequest): Promise<SubscriptionInput> => {
12
+ console.warn(
13
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
14
+ );
15
+ const chargebee = await client.getFromEnv();
16
+ // https://api-explorer.chargebee.com/item_prices/list_item_prices
17
+ const { list } = await chargebee.itemPrice.list({
18
+ limit: 1,
19
+ item_type: {
20
+ is: "plan",
21
+ },
22
+ status: {
23
+ is: "active",
24
+ },
25
+ });
26
+
27
+ return {
28
+ subscription_items: [{ item_price_id: list[0]?.item_price.id }],
29
+ redirect_url: `${req.nextUrl.origin}{{pathPrefix}}/checkout/callback`,
30
+ } as SubscriptionInput;
31
+ },
32
+ });
@@ -0,0 +1,19 @@
1
+ import { createPortalSession, type PortalCreateInput } from "@chargebee/nextjs";
2
+ import type { NextRequest } from "next/server.js";
3
+
4
+ export const GET = createPortalSession({
5
+ apiKey: process.env.CHARGEBEE_API_KEY!,
6
+ site: process.env.CHARGEBEE_SITE!,
7
+ apiPayload: async (req: NextRequest): Promise<PortalCreateInput> => {
8
+ console.warn(
9
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
10
+ );
11
+ // TODO: Return the authenticated customer here
12
+ return {
13
+ customer: {
14
+ id: "",
15
+ },
16
+ redirect_url: `${req.nextUrl.origin}/users/`,
17
+ } as PortalCreateInput;
18
+ },
19
+ });
@@ -0,0 +1,25 @@
1
+ import { type Chargebee, validateBasicAuth } from "@chargebee/nextjs";
2
+ import { type NextRequest, NextResponse } from "next/server.js";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ console.warn(
6
+ `⚠ This is the default implementation from chargebee-init and must be reviewed!`,
7
+ );
8
+ // HTTP Basic Auth is currently optional when adding a new webhook
9
+ // url in the Chargebee dashboard. However, we expect it's set by default.
10
+ // Please set the env variable CHARGEBEE_WEBHOOK_BASIC_AUTH to "user:pass"
11
+ // which is validated here
12
+ try {
13
+ validateBasicAuth(
14
+ process.env.CHARGEBEE_WEBHOOK_AUTH,
15
+ req.headers.get("authorization"),
16
+ );
17
+ } catch (error) {
18
+ console.error(error);
19
+ return NextResponse.error();
20
+ }
21
+
22
+ const data = (await req.json()) as Chargebee.Event;
23
+ // TODO: handle the incoming webhook data
24
+ console.log(data);
25
+ }
@@ -0,0 +1,7 @@
1
+ import type { Framework, FrameworkInfo } from "./frameworks.js";
2
+ export declare const copyTemplates: ({ targetDir, framework, frameworkInfo, pathPrefix, }: {
3
+ targetDir: string;
4
+ framework: Framework;
5
+ frameworkInfo: FrameworkInfo;
6
+ pathPrefix: string;
7
+ }) => string[];
@@ -0,0 +1,52 @@
1
+ var _a;
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const __dirname = (_a = globalThis.__dirname) !== null && _a !== void 0 ? _a : import.meta.dirname;
5
+ export const copyTemplates = ({ targetDir, framework, frameworkInfo, pathPrefix = "", }) => {
6
+ const srcDir = path.join(__dirname, "templates", framework);
7
+ // Determine app directory to write files to
8
+ let appDir = targetDir;
9
+ for (let dir of frameworkInfo.appDirectories) {
10
+ dir = path.join(appDir, dir);
11
+ if (fs.existsSync(dir)) {
12
+ appDir = dir;
13
+ break;
14
+ }
15
+ }
16
+ if (appDir === targetDir || !fs.existsSync(appDir)) {
17
+ throw new Error(`Could not find expected directories to copy files to: ${frameworkInfo.appDirectories.join(", ")}`);
18
+ }
19
+ const copiedFiles = [];
20
+ const templatesDir = path.join(appDir, pathPrefix);
21
+ copyDirectory(srcDir, templatesDir, { pathPrefix }, copiedFiles);
22
+ return copiedFiles;
23
+ };
24
+ function replaceVariables(text, replacements) {
25
+ return text.replace(/{{\s*(\w+)\s*}}/g, (_, key) => { var _a; return (_a = replacements[key]) !== null && _a !== void 0 ? _a : ""; });
26
+ }
27
+ // Recursively copy the directory and replace templated strings
28
+ function copyDirectory(srcDir, destDir, replacements, copiedFiles) {
29
+ // Ensure destination directory exists
30
+ if (!fs.existsSync(destDir)) {
31
+ fs.mkdirSync(destDir, { recursive: true });
32
+ }
33
+ // Read all items in the current source directory
34
+ const items = fs.readdirSync(srcDir, { withFileTypes: true });
35
+ for (const item of items) {
36
+ const srcPath = path.join(srcDir, item.name);
37
+ const destPath = path.join(destDir, item.name);
38
+ if (item.isDirectory()) {
39
+ // Recursively copy subdirectory
40
+ copyDirectory(srcPath, destPath, replacements, copiedFiles);
41
+ }
42
+ else if (item.isFile()) {
43
+ let content = fs.readFileSync(srcPath, "utf-8");
44
+ // Only perform replacements for .ts files
45
+ if (path.extname(item.name) === ".ts") {
46
+ content = replaceVariables(content, replacements);
47
+ }
48
+ fs.writeFileSync(destPath, content, "utf-8");
49
+ copiedFiles.push(destPath);
50
+ }
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "chargebee-init",
3
+ "version": "1.0.0-beta.1",
4
+ "author": {
5
+ "name": "Chargebee",
6
+ "email": "dx@chargebee.com"
7
+ },
8
+ "description": "A CLI to help integrate Chargebee services with your existing app",
9
+ "type": "module",
10
+ "exports": "./dist/cli.js",
11
+ "bin": {
12
+ "chargebee-init": "bin/chargebee-init"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "bin"
20
+ ],
21
+ "dependencies": {
22
+ "ansi-colors": "^4.1.3",
23
+ "enquirer": "^2.4.1",
24
+ "meow": "^13.2.0",
25
+ "simple-git": "^3.27.0",
26
+ "semver": "^7.7.2"
27
+ },
28
+ "devDependencies": {
29
+ "@pnpm/types": "^1000.6.0",
30
+ "@types/semver": "^7.7.0"
31
+ },
32
+ "scripts": {
33
+ "prebuild": "rm -rf dist && mkdir -p dist/templates",
34
+ "build": "tsc",
35
+ "postbuild": "for n in nextjs express; do cp -R ../$n/templates dist/templates/$n/; done;",
36
+ "watch": "pnpm build && tsc --watch"
37
+ }
38
+ }