mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-10 12:47:47 +00:00
working checkouts
This commit is contained in:
7
example/convex/_generated/api.d.ts
vendored
7
example/convex/_generated/api.d.ts
vendored
@@ -96,7 +96,6 @@ export declare const components: {
|
||||
"mutation",
|
||||
"internal",
|
||||
{
|
||||
callback?: string;
|
||||
subscription: {
|
||||
amount: number | null;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
@@ -204,6 +203,12 @@ export declare const components: {
|
||||
userId: string;
|
||||
} | null
|
||||
>;
|
||||
insertCustomer: FunctionReference<
|
||||
"mutation",
|
||||
"internal",
|
||||
{ id: string; userId: string },
|
||||
string
|
||||
>;
|
||||
listProducts: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
|
||||
@@ -11,10 +11,15 @@ export const MAX_FREE_TODOS = 3;
|
||||
export const MAX_PREMIUM_TODOS = 6;
|
||||
|
||||
export const { generateCheckoutLink } = polar.checkoutApi({
|
||||
products: {
|
||||
premium: "5fde8344-5fca-4d0b-adeb-2052cddfd9ed",
|
||||
premiumPlus: "db548a6f-ff8c-4969-8f02-5f7301a36e7c",
|
||||
},
|
||||
getUserInfo: async (ctx) => {
|
||||
const user = await ctx.runQuery(api.example.getCurrentUser);
|
||||
return {
|
||||
userId: user._id,
|
||||
email: user.email,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,6 +58,7 @@ const seed = internalAction({
|
||||
recurringInterval: "month",
|
||||
prices: [
|
||||
{
|
||||
amountType: "fixed",
|
||||
priceAmount: 1000,
|
||||
},
|
||||
],
|
||||
@@ -68,6 +69,7 @@ const seed = internalAction({
|
||||
recurringInterval: "year",
|
||||
prices: [
|
||||
{
|
||||
amountType: "fixed",
|
||||
priceAmount: 10000,
|
||||
},
|
||||
],
|
||||
@@ -78,6 +80,7 @@ const seed = internalAction({
|
||||
recurringInterval: "month",
|
||||
prices: [
|
||||
{
|
||||
amountType: "fixed",
|
||||
priceAmount: 2000,
|
||||
},
|
||||
],
|
||||
@@ -88,6 +91,7 @@ const seed = internalAction({
|
||||
recurringInterval: "year",
|
||||
prices: [
|
||||
{
|
||||
amountType: "fixed",
|
||||
priceAmount: 20000,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -74,10 +74,7 @@ export function UpgradeCTA({
|
||||
</li>
|
||||
</ul>
|
||||
{!isPremium && !isPremiumPlus && (
|
||||
<CheckoutLink
|
||||
polarApi={api.example}
|
||||
productId="2d368710-520b-49b0-ba7b-9c2d60d7b1c2"
|
||||
>
|
||||
<CheckoutLink polarApi={api.example} productKey="premium">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white text-indigo-700 hover:bg-gray-100 dark:bg-gray-800 dark:text-indigo-300 dark:hover:bg-gray-700"
|
||||
@@ -142,10 +139,7 @@ export function UpgradeCTA({
|
||||
</li>
|
||||
</ul>
|
||||
{!isPremiumPlus && (
|
||||
<CheckoutLink
|
||||
polarApi={api.example}
|
||||
productId="premium-plus-product-id"
|
||||
>
|
||||
<CheckoutLink polarApi={api.example} productKey="premiumPlus">
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="w-full bg-white/95 backdrop-blur-sm text-purple-700 hover:bg-white dark:bg-white/10 dark:text-purple-200 dark:hover:bg-white/20"
|
||||
|
||||
@@ -2,13 +2,11 @@ import "./polyfill";
|
||||
import {
|
||||
ApiFromModules,
|
||||
FunctionReference,
|
||||
GenericActionCtx,
|
||||
GenericDataModel,
|
||||
type HttpRouter,
|
||||
WithoutSystemFields,
|
||||
actionGeneric,
|
||||
createFunctionHandle,
|
||||
httpActionGeneric,
|
||||
GenericActionCtx,
|
||||
} from "convex/server";
|
||||
import {
|
||||
type ComponentApi,
|
||||
@@ -18,17 +16,18 @@ import {
|
||||
RunQueryCtx,
|
||||
} from "../component/util";
|
||||
import { Polar as PolarSdk } from "@polar-sh/sdk";
|
||||
import { Doc } from "../component/_generated/dataModel";
|
||||
import { Infer, v } from "convex/values";
|
||||
import schema from "../component/schema";
|
||||
import { WebhookSubscriptionCreatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptioncreatedpayload.js";
|
||||
import { WebhookSubscriptionUpdatedPayload } from "@polar-sh/sdk/models/components/webhooksubscriptionupdatedpayload.js";
|
||||
import { WebhookProductCreatedPayload } from "@polar-sh/sdk/models/components/webhookproductcreatedpayload.js";
|
||||
import { WebhookProductUpdatedPayload } from "@polar-sh/sdk/models/components/webhookproductupdatedpayload.js";
|
||||
import { Checkout } from "@polar-sh/sdk/models/components/checkout.js";
|
||||
import {
|
||||
validateEvent,
|
||||
WebhookVerificationError,
|
||||
} from "@polar-sh/sdk/webhooks";
|
||||
import { EmptyObject } from "convex-helpers";
|
||||
|
||||
export const subscriptionValidator = schema.tables.subscriptions.validator;
|
||||
export type Subscription = Infer<typeof subscriptionValidator>;
|
||||
@@ -45,14 +44,14 @@ export type CheckoutApi<DataModel extends GenericDataModel> = ApiFromModules<{
|
||||
|
||||
export class Polar<DataModel extends GenericDataModel> {
|
||||
private polar: PolarSdk;
|
||||
public onScheduleCreated?: SubscriptionHandler;
|
||||
constructor(
|
||||
public component: ComponentApi,
|
||||
options: {
|
||||
onScheduleCreated?: FunctionReference<
|
||||
"mutation",
|
||||
private options: {
|
||||
getUserInfo?: FunctionReference<
|
||||
"query",
|
||||
"internal",
|
||||
{ subscription: WithoutSystemFields<Doc<"subscriptions">> }
|
||||
EmptyObject,
|
||||
{ userId: string; email?: string }
|
||||
>;
|
||||
} = {}
|
||||
) {
|
||||
@@ -61,41 +60,48 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
server:
|
||||
(process.env["POLAR_SERVER"] as "sandbox" | "production") ?? "sandbox",
|
||||
});
|
||||
this.onScheduleCreated = options.onScheduleCreated;
|
||||
}
|
||||
getCustomerByUserId(ctx: RunQueryCtx, userId: string) {
|
||||
return ctx.runQuery(this.component.lib.getCustomerByUserId, { userId });
|
||||
}
|
||||
/*
|
||||
async createCheckoutSession(
|
||||
ctx: GenericActionCtx<DataModel>,
|
||||
{
|
||||
productId,
|
||||
userId,
|
||||
email,
|
||||
}: { productId: string; userId: string; email: string }
|
||||
) {
|
||||
const customer =
|
||||
(await ctx.runQuery(this.component.lib.getCustomerByUserId, {
|
||||
origin,
|
||||
}: { productId: string; userId: string; email: string; origin: string }
|
||||
): Promise<Checkout> {
|
||||
const dbCustomer = await ctx.runQuery(
|
||||
this.component.lib.getCustomerByUserId,
|
||||
{
|
||||
userId,
|
||||
})) ||
|
||||
(await this.polar.customers.create({
|
||||
email,
|
||||
metadata: {
|
||||
userId,
|
||||
},
|
||||
}));
|
||||
const customerId = customer?.id;
|
||||
if (!customerId) {
|
||||
throw new Error("Customer not found");
|
||||
}
|
||||
);
|
||||
const customerId =
|
||||
dbCustomer?.id ||
|
||||
(
|
||||
await this.polar.customers.create({
|
||||
email,
|
||||
metadata: {
|
||||
userId,
|
||||
},
|
||||
})
|
||||
).id;
|
||||
if (!dbCustomer) {
|
||||
await ctx.runMutation(this.component.lib.insertCustomer, {
|
||||
id: customerId,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
return this.polar.checkouts.custom.create({
|
||||
return this.polar.checkouts.create({
|
||||
allowDiscountCodes: true,
|
||||
productId,
|
||||
products: [productId],
|
||||
customerId,
|
||||
embedOrigin: origin,
|
||||
});
|
||||
}
|
||||
*/
|
||||
listProducts(
|
||||
ctx: RunQueryCtx,
|
||||
{ includeArchived }: { includeArchived: boolean }
|
||||
@@ -117,15 +123,16 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
return ctx.runQuery(this.component.lib.getProduct, { id: productId });
|
||||
}
|
||||
checkoutApi(opts: {
|
||||
products: Record<string, string>;
|
||||
getUserInfo: (ctx: RunQueryCtx) => Promise<{
|
||||
userId: string;
|
||||
email?: string;
|
||||
email: string;
|
||||
}>;
|
||||
}) {
|
||||
return {
|
||||
generateCheckoutLink: actionGeneric({
|
||||
args: {
|
||||
productId: v.string(),
|
||||
productKey: v.string(),
|
||||
origin: v.string(),
|
||||
},
|
||||
returns: v.object({
|
||||
@@ -133,17 +140,13 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
}),
|
||||
handler: async (ctx, args) => {
|
||||
const { userId, email } = await opts.getUserInfo(ctx);
|
||||
const { url } = await this.polar.checkouts.create({
|
||||
productId: args.productId,
|
||||
embedOrigin: args.origin,
|
||||
customerEmail: email,
|
||||
metadata: {
|
||||
userId,
|
||||
},
|
||||
const { url } = await this.createCheckoutSession(ctx, {
|
||||
productId: opts.products?.[args.productKey],
|
||||
userId,
|
||||
email,
|
||||
origin: args.origin,
|
||||
});
|
||||
return {
|
||||
url,
|
||||
};
|
||||
return { url };
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -194,9 +197,6 @@ export class Polar<DataModel extends GenericDataModel> {
|
||||
event.data.metadata.userId as string,
|
||||
event.data
|
||||
),
|
||||
callback:
|
||||
this.onScheduleCreated &&
|
||||
(await createFunctionHandle(this.onScheduleCreated)),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
7
src/component/_generated/api.d.ts
vendored
7
src/component/_generated/api.d.ts
vendored
@@ -82,7 +82,6 @@ export type Mounts = {
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
callback?: string;
|
||||
subscription: {
|
||||
amount: number | null;
|
||||
cancelAtPeriodEnd: boolean;
|
||||
@@ -185,6 +184,12 @@ export type Mounts = {
|
||||
userId: string;
|
||||
} | null
|
||||
>;
|
||||
insertCustomer: FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{ id: string; userId: string },
|
||||
string
|
||||
>;
|
||||
listProducts: FunctionReference<
|
||||
"query",
|
||||
"public",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { v, VString } from "convex/values";
|
||||
import { v } from "convex/values";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import schema from "./schema";
|
||||
import { asyncMap } from "convex-helpers";
|
||||
import { FunctionHandle, WithoutSystemFields } from "convex/server";
|
||||
import { Doc } from "./_generated/dataModel";
|
||||
|
||||
export const getCustomerByUserId = query({
|
||||
args: {
|
||||
@@ -25,6 +23,20 @@ export const getCustomerByUserId = query({
|
||||
},
|
||||
});
|
||||
|
||||
export const insertCustomer = mutation({
|
||||
args: {
|
||||
id: v.string(),
|
||||
userId: v.string(),
|
||||
},
|
||||
returns: v.id("customers"),
|
||||
handler: async (ctx, args) => {
|
||||
return ctx.db.insert("customers", {
|
||||
id: args.id,
|
||||
userId: args.userId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const upsertCustomer = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
@@ -163,21 +175,12 @@ export const listProducts = query({
|
||||
},
|
||||
});
|
||||
|
||||
type Subscription = WithoutSystemFields<Doc<"subscriptions">>;
|
||||
const subscriptionCallbackValidator = v.string() as VString<
|
||||
FunctionHandle<"mutation", { subscription: Subscription }>
|
||||
>;
|
||||
|
||||
export const createSubscription = mutation({
|
||||
args: {
|
||||
subscription: schema.tables.subscriptions.validator,
|
||||
callback: v.optional(subscriptionCallbackValidator),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.insert("subscriptions", args.subscription);
|
||||
if (args.callback) {
|
||||
await ctx.runMutation(args.callback, { subscription: args.subscription });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ import { useAction } from "convex/react";
|
||||
|
||||
export const CheckoutLink = <DataModel extends GenericDataModel>({
|
||||
polarApi,
|
||||
productId,
|
||||
productKey,
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<{
|
||||
polarApi: CheckoutApi<DataModel>;
|
||||
productId: string;
|
||||
productKey: string;
|
||||
className?: string;
|
||||
}>) => {
|
||||
const generateCheckoutLink = useAction(polarApi.generateCheckoutLink);
|
||||
@@ -20,7 +20,7 @@ export const CheckoutLink = <DataModel extends GenericDataModel>({
|
||||
useEffect(() => {
|
||||
PolarEmbedCheckout.init();
|
||||
void generateCheckoutLink({
|
||||
productId,
|
||||
productKey,
|
||||
origin: window.location.origin,
|
||||
}).then(({ url }) => setCheckoutLink(url));
|
||||
}, []);
|
||||
@@ -30,7 +30,7 @@ export const CheckoutLink = <DataModel extends GenericDataModel>({
|
||||
className={className}
|
||||
href={checkoutLink}
|
||||
data-polar-checkout
|
||||
//data-polar-checkout-theme="light"
|
||||
data-polar-checkout-theme="dark"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user