@faire-auth/stripe 1.0.0

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/src/types.ts ADDED
@@ -0,0 +1,355 @@
1
+ import type { ContextVars, InferOptionSchema } from "faire-auth";
2
+ import type { Context } from "@faire-auth/core/types";
3
+ import type Stripe from "stripe";
4
+ import type { subscriptions, user } from "./schema";
5
+ import type { Session, User } from "faire-auth/db";
6
+
7
+ export type StripePlan = {
8
+ /**
9
+ * Monthly price id
10
+ */
11
+ priceId?: string;
12
+ /**
13
+ * To use lookup key instead of price id
14
+ *
15
+ * https://docs.stripe.com/products-prices/
16
+ * manage-prices#lookup-keys
17
+ */
18
+ lookupKey?: string;
19
+ /**
20
+ * A yearly discount price id
21
+ *
22
+ * useful when you want to offer a discount for
23
+ * yearly subscription
24
+ */
25
+ annualDiscountPriceId?: string;
26
+ /**
27
+ * To use lookup key instead of price id
28
+ *
29
+ * https://docs.stripe.com/products-prices/
30
+ * manage-prices#lookup-keys
31
+ */
32
+ annualDiscountLookupKey?: string;
33
+ /**
34
+ * Plan name
35
+ */
36
+ name: string;
37
+ /**
38
+ * Limits for the plan
39
+ */
40
+ limits?: Record<string, number>;
41
+ /**
42
+ * Plan group name
43
+ *
44
+ * useful when you want to group plans or
45
+ * when a user can subscribe to multiple plans.
46
+ */
47
+ group?: string;
48
+ /**
49
+ * Free trial days
50
+ */
51
+ freeTrial?: {
52
+ /**
53
+ * Number of days
54
+ */
55
+ days: number;
56
+ /**
57
+ * A function that will be called when the trial
58
+ * starts.
59
+ *
60
+ * @param subscription
61
+ * @returns
62
+ */
63
+ onTrialStart?: (subscription: Subscription) => Promise<void>;
64
+ /**
65
+ * A function that will be called when the trial
66
+ * ends
67
+ *
68
+ * @param subscription - Subscription
69
+ * @returns
70
+ */
71
+ onTrialEnd?: <V extends object>(
72
+ data: {
73
+ subscription: Subscription;
74
+ },
75
+ ctx: Context<ContextVars<V>>,
76
+ ) => Promise<void>;
77
+ /**
78
+ * A function that will be called when the trial
79
+ * expired.
80
+ * @param subscription - Subscription
81
+ * @returns
82
+ */
83
+ onTrialExpired?: <V extends object>(
84
+ subscription: Subscription,
85
+ ctx: Context<ContextVars<V>>,
86
+ ) => Promise<void>;
87
+ };
88
+ };
89
+
90
+ export interface Subscription {
91
+ /**
92
+ * Database identifier
93
+ */
94
+ id: string;
95
+ /**
96
+ * The plan name
97
+ */
98
+ plan: string;
99
+ /**
100
+ * Stripe customer id
101
+ */
102
+ stripeCustomerId?: string;
103
+ /**
104
+ * Stripe subscription id
105
+ */
106
+ stripeSubscriptionId?: string;
107
+ /**
108
+ * Trial start date
109
+ */
110
+ trialStart?: Date;
111
+ /**
112
+ * Trial end date
113
+ */
114
+ trialEnd?: Date;
115
+ /**
116
+ * Price Id for the subscription
117
+ */
118
+ priceId?: string;
119
+ /**
120
+ * To what reference id the subscription belongs to
121
+ * @example
122
+ * - userId for a user
123
+ * - workspace id for a saas platform
124
+ * - website id for a hosting platform
125
+ *
126
+ * @default - userId
127
+ */
128
+ referenceId: string;
129
+ /**
130
+ * Subscription status
131
+ */
132
+ status:
133
+ | "active"
134
+ | "canceled"
135
+ | "incomplete"
136
+ | "incomplete_expired"
137
+ | "past_due"
138
+ | "paused"
139
+ | "trialing"
140
+ | "unpaid";
141
+ /**
142
+ * The billing cycle start date
143
+ */
144
+ periodStart?: Date;
145
+ /**
146
+ * The billing cycle end date
147
+ */
148
+ periodEnd?: Date;
149
+ /**
150
+ * Whether this subscription will (if status=active)
151
+ * or did (if status=canceled) cancel at the end of the current billing period.
152
+ */
153
+ cancelAtPeriodEnd?: boolean;
154
+ /**
155
+ * If the subscription is scheduled to be canceled,
156
+ * this is the time at which the cancellation will take effect.
157
+ */
158
+ cancelAt?: Date;
159
+ /**
160
+ * If the subscription has been canceled, this is the time when it was canceled.
161
+ *
162
+ * Note: If the subscription was canceled with `cancel_at_period_end`,
163
+ * this reflects the cancellation request time, not when the subscription actually ends.
164
+ */
165
+ canceledAt?: Date;
166
+ /**
167
+ * If the subscription has ended, the date the subscription ended.
168
+ */
169
+ endedAt?: Date;
170
+ /**
171
+ * A field to group subscriptions so you can have multiple subscriptions
172
+ * for one reference id
173
+ */
174
+ groupId?: string;
175
+ /**
176
+ * Number of seats for the subscription (useful for team plans)
177
+ */
178
+ seats?: number;
179
+ }
180
+
181
+ export interface SubscriptionOptions {
182
+ enabled: boolean;
183
+ /**
184
+ * Subscription Configuration
185
+ */
186
+ /**
187
+ * List of plan
188
+ */
189
+ plans: StripePlan[] | (() => StripePlan[] | Promise<StripePlan[]>);
190
+ /**
191
+ * Require email verification before a user is allowed to upgrade
192
+ * their subscriptions
193
+ *
194
+ * @default false
195
+ */
196
+ requireEmailVerification?: boolean;
197
+ /**
198
+ * A callback to run after a user has subscribed to a package
199
+ * @param event - Stripe Event
200
+ * @param subscription - Subscription Data
201
+ * @returns
202
+ */
203
+ onSubscriptionComplete?: <V extends object>(
204
+ data: {
205
+ event: Stripe.Event;
206
+ stripeSubscription: Stripe.Subscription;
207
+ subscription: Subscription;
208
+ plan: StripePlan;
209
+ },
210
+ ctx: Context<ContextVars<V>>,
211
+ ) => Promise<void>;
212
+ /**
213
+ * A callback to run after a user is about to cancel their subscription
214
+ * @returns
215
+ */
216
+ onSubscriptionUpdate?: (data: {
217
+ event: Stripe.Event;
218
+ subscription: Subscription;
219
+ }) => Promise<void>;
220
+ /**
221
+ * A callback to run after a user is about to cancel their subscription
222
+ * @returns
223
+ */
224
+ onSubscriptionCancel?: (data: {
225
+ event?: Stripe.Event | undefined;
226
+ subscription: Subscription;
227
+ stripeSubscription: Stripe.Subscription;
228
+ cancellationDetails?: Stripe.Subscription.CancellationDetails | null;
229
+ }) => Promise<void>;
230
+ /**
231
+ * A function to check if the reference id is valid
232
+ * and belongs to the user
233
+ *
234
+ * @param data - data containing user, session and referenceId
235
+ * @param ctx - the context object
236
+ * @returns
237
+ */
238
+ authorizeReference?: <V extends object>(
239
+ data: {
240
+ user: User;
241
+ session: Session;
242
+ referenceId: string;
243
+ action:
244
+ | "upgrade-subscription"
245
+ | "list-subscription"
246
+ | "cancel-subscription"
247
+ | "restore-subscription"
248
+ | "billing-portal";
249
+ },
250
+ ctx: Context<ContextVars<V>>,
251
+ ) => Promise<boolean>;
252
+ /**
253
+ * A callback to run after a user has deleted their subscription
254
+ * @returns
255
+ */
256
+ onSubscriptionDeleted?: (data: {
257
+ event: Stripe.Event;
258
+ stripeSubscription: Stripe.Subscription;
259
+ subscription: Subscription;
260
+ }) => Promise<void>;
261
+ /**
262
+ * parameters for session create params
263
+ *
264
+ * @param data - data containing user, session and plan
265
+ * @param ctx - the context object
266
+ */
267
+ getCheckoutSessionParams?: <V extends object>(
268
+ data: {
269
+ user: User;
270
+ session: Session;
271
+ plan: StripePlan;
272
+ subscription: Subscription;
273
+ },
274
+ ctx: Context<ContextVars<V>>,
275
+ ) =>
276
+ | Promise<{
277
+ params?: Stripe.Checkout.SessionCreateParams;
278
+ options?: Stripe.RequestOptions;
279
+ }>
280
+ | {
281
+ params?: Stripe.Checkout.SessionCreateParams;
282
+ options?: Stripe.RequestOptions;
283
+ };
284
+ /**
285
+ * Enable organization subscription
286
+ */
287
+ organization?: {
288
+ enabled: boolean;
289
+ };
290
+ }
291
+
292
+ export interface StripeOptions {
293
+ /**
294
+ * Stripe Client
295
+ */
296
+ stripeClient: Stripe;
297
+ /**
298
+ * Stripe Webhook Secret
299
+ *
300
+ * @description Stripe webhook secret key
301
+ */
302
+ stripeWebhookSecret: string;
303
+ /**
304
+ * Enable customer creation when a user signs up
305
+ */
306
+ createCustomerOnSignUp?: boolean;
307
+ /**
308
+ * A callback to run after a customer has been created
309
+ * @param customer - Customer Data
310
+ * @param stripeCustomer - Stripe Customer Data
311
+ * @returns
312
+ */
313
+ onCustomerCreate?: <V extends object>(
314
+ data: {
315
+ stripeCustomer: Stripe.Customer;
316
+ user: User;
317
+ },
318
+ ctx: Context<ContextVars<V>>,
319
+ ) => Promise<void>;
320
+ /**
321
+ * A custom function to get the customer create
322
+ * params
323
+ * @param data - data containing user and session
324
+ * @returns
325
+ */
326
+ getCustomerCreateParams?: <V extends object>(
327
+ data: {
328
+ user: User;
329
+ session: Session;
330
+ },
331
+ ctx: Context<ContextVars<V>>,
332
+ ) => Promise<{}>;
333
+ /**
334
+ * Subscriptions
335
+ */
336
+ subscription?:
337
+ | {
338
+ enabled: false;
339
+ }
340
+ | ({
341
+ enabled: true;
342
+ } & SubscriptionOptions);
343
+ /**
344
+ * A callback to run after a stripe event is received
345
+ * @param event - Stripe Event
346
+ * @returns
347
+ */
348
+ onEvent?: (event: Stripe.Event) => Promise<void>;
349
+ /**
350
+ * Schema for the stripe plugin
351
+ */
352
+ schema?: InferOptionSchema<typeof subscriptions & typeof user>;
353
+ }
354
+
355
+ export interface InputSubscription extends Omit<Subscription, "id"> {}
package/src/utils.ts ADDED
@@ -0,0 +1,70 @@
1
+ import type Stripe from "stripe";
2
+ import type { StripeOptions, Subscription } from "./types";
3
+
4
+ export async function getPlans(
5
+ subscriptionOptions: StripeOptions["subscription"],
6
+ ) {
7
+ if (subscriptionOptions?.enabled) {
8
+ return typeof subscriptionOptions.plans === "function"
9
+ ? await subscriptionOptions.plans()
10
+ : subscriptionOptions.plans;
11
+ }
12
+ throw new Error("Subscriptions are not enabled in the Stripe options.");
13
+ }
14
+
15
+ export async function getPlanByPriceInfo(
16
+ options: StripeOptions,
17
+ priceId: string,
18
+ priceLookupKey: string | null,
19
+ ) {
20
+ return await getPlans(options.subscription).then((res) =>
21
+ res?.find(
22
+ (plan) =>
23
+ plan.priceId === priceId ||
24
+ plan.annualDiscountPriceId === priceId ||
25
+ (priceLookupKey &&
26
+ (plan.lookupKey === priceLookupKey ||
27
+ plan.annualDiscountLookupKey === priceLookupKey)),
28
+ ),
29
+ );
30
+ }
31
+
32
+ export async function getPlanByName(options: StripeOptions, name: string) {
33
+ return await getPlans(options.subscription).then((res) =>
34
+ res?.find((plan) => plan.name.toLowerCase() === name.toLowerCase()),
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Checks if a subscription is in an available state (active or trialing)
40
+ */
41
+ export function isActiveOrTrialing(
42
+ sub: Subscription | Stripe.Subscription,
43
+ ): boolean {
44
+ return sub.status === "active" || sub.status === "trialing";
45
+ }
46
+
47
+ /**
48
+ * Check if a subscription is scheduled to be canceled (DB subscription object)
49
+ */
50
+ export function isPendingCancel(sub: Subscription): boolean {
51
+ return !!(sub.cancelAtPeriodEnd || sub.cancelAt);
52
+ }
53
+
54
+ /**
55
+ * Check if a Stripe subscription is scheduled to be canceled (Stripe API response)
56
+ */
57
+ export function isStripePendingCancel(stripeSub: Stripe.Subscription): boolean {
58
+ return !!(stripeSub.cancel_at_period_end || stripeSub.cancel_at);
59
+ }
60
+
61
+ /**
62
+ * Escapes a value for use in Stripe search queries.
63
+ * Stripe search query uses double quotes for string values,
64
+ * and double quotes within the value need to be escaped with backslash.
65
+ *
66
+ * @see https://docs.stripe.com/search#search-query-language
67
+ */
68
+ export function escapeStripeSearchValue(value: string): string {
69
+ return value.replace(/"/g, '\\"');
70
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig.json",
3
+ "extends": "../../tsconfig.base.json",
4
+ "compilerOptions": {
5
+ "lib": ["esnext", "dom", "dom.iterable"]
6
+ },
7
+ "include": ["src/*.ts", "overrides.d.ts"],
8
+ // "exclude": ["node_modules", "**/*.test.ts"],
9
+ "references": [
10
+ {
11
+ "path": "../faire-auth/tsconfig.json"
12
+ },
13
+ {
14
+ "path": "../core/tsconfig.json"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,16 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ dts: { build: true, incremental: true },
5
+ format: ["esm", "cjs"],
6
+ entry: ["./src/index.ts", "./src/client.ts"],
7
+ external: [
8
+ "faire-auth",
9
+ "better-call",
10
+ "@better-fetch/fetch",
11
+ "stripe",
12
+ "hono",
13
+ ],
14
+ nodeProtocol: true,
15
+ publint: true,
16
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ root: ".",
5
+ test: {
6
+ clearMocks: true,
7
+ globals: true,
8
+ },
9
+ });