mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-07 12:47:47 +00:00
add schema, client methods
This commit is contained in:
665
example/convex/_generated/api.d.ts
vendored
665
example/convex/_generated/api.d.ts
vendored
@@ -39,110 +39,609 @@ export declare const internal: FilterApi<
|
|||||||
|
|
||||||
export declare const components: {
|
export declare const components: {
|
||||||
polar: {
|
polar: {
|
||||||
init: {
|
lib: {
|
||||||
seedProducts: FunctionReference<
|
getBenefit: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getBenefitGrant: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getOrder: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getProduct: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getSubscription: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
insertBenefit: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
benefit: {
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertBenefitGrant: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
benefitGrant: {
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertOrder: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertProduct: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertSubscription: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
subscription: {
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
listBenefits: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
any,
|
||||||
|
Array<{
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listPlans: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ includeArchived: boolean },
|
||||||
|
Array<{
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listUserBenefitGrants: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
Array<{
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listUserSubscriptions: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{ userId: string },
|
||||||
|
Array<{
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
product?: {
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
pullProducts: FunctionReference<
|
||||||
"action",
|
"action",
|
||||||
"internal",
|
"internal",
|
||||||
{ polarAccessToken: string; polarOrganizationId: string },
|
{ polarAccessToken: string; polarOrganizationId: string },
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
};
|
updateBenefit: FunctionReference<
|
||||||
lib: {
|
|
||||||
createUser: FunctionReference<
|
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
deleteUserSubscription: FunctionReference<
|
|
||||||
"mutation",
|
|
||||||
"internal",
|
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getOnboardingCheckoutUrl: FunctionReference<
|
|
||||||
"action",
|
|
||||||
"internal",
|
|
||||||
{
|
{
|
||||||
polarAccessToken: string;
|
benefit: {
|
||||||
successUrl: string;
|
createdAt: string;
|
||||||
userEmail?: string;
|
deletable: boolean;
|
||||||
userId: string;
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
getPlanByKey: FunctionReference<
|
updateBenefitGrant: FunctionReference<
|
||||||
"query",
|
|
||||||
"internal",
|
|
||||||
{ key: "free" | "pro" },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getProOnboardingCheckoutUrl: FunctionReference<
|
|
||||||
"action",
|
|
||||||
"internal",
|
|
||||||
{
|
|
||||||
interval: "month" | "year";
|
|
||||||
polarAccessToken: string;
|
|
||||||
successUrl: string;
|
|
||||||
userId: string;
|
|
||||||
},
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getUser: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"internal",
|
|
||||||
{ userId: string },
|
|
||||||
null | {
|
|
||||||
polarId?: string;
|
|
||||||
subscription?: {
|
|
||||||
cancelAtPeriodEnd?: boolean;
|
|
||||||
currency: "usd" | "eur";
|
|
||||||
currentPeriodEnd?: number;
|
|
||||||
currentPeriodStart?: number;
|
|
||||||
interval: "month" | "year";
|
|
||||||
localUserId: string;
|
|
||||||
planId: string;
|
|
||||||
polarId: string;
|
|
||||||
polarPriceId: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
subscriptionIsPending?: boolean;
|
|
||||||
subscriptionPendingId?: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
getUserByLocalId: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"internal",
|
|
||||||
{ localUserId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
listPlans: FunctionReference<"query", "internal", {}, any>;
|
|
||||||
replaceSubscription: FunctionReference<
|
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{
|
{
|
||||||
input: {
|
benefitGrant: {
|
||||||
cancelAtPeriodEnd?: boolean;
|
benefitId: string;
|
||||||
currency: "usd" | "eur";
|
createdAt: string;
|
||||||
currentPeriodEnd?: number;
|
grantedAt: string | null;
|
||||||
currentPeriodStart: number;
|
id: string;
|
||||||
interval: "month" | "year";
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateOrder: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateProduct: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateProducts: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
polarAccessToken: string;
|
||||||
|
products: Array<{
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateSubscription: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
subscription: {
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
priceId: string;
|
priceId: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
userId: string;
|
||||||
};
|
};
|
||||||
localUserId: string;
|
|
||||||
subscriptionPolarId: string;
|
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
setSubscriptionPending: FunctionReference<
|
|
||||||
"mutation",
|
|
||||||
"internal",
|
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -67,7 +67,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"convex": "^1.17.0"
|
"convex": "^1.17.0",
|
||||||
|
"@polar-sh/sdk": "^0.13.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
@@ -85,9 +86,6 @@
|
|||||||
"types": "./dist/commonjs/client/index.d.ts",
|
"types": "./dist/commonjs/client/index.d.ts",
|
||||||
"module": "./dist/esm/client/index.js",
|
"module": "./dist/esm/client/index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/auth": "^0.0.74",
|
|
||||||
"@polar-sh/sdk": "^0.13.5",
|
|
||||||
"@react-email/components": "0.0.26",
|
|
||||||
"convex-helpers": "^0.1.63",
|
"convex-helpers": "^0.1.63",
|
||||||
"standardwebhooks": "^1.0.0"
|
"standardwebhooks": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +1,188 @@
|
|||||||
import { type HttpRouter, httpActionGeneric } from "convex/server";
|
|
||||||
import {
|
import {
|
||||||
ComponentApi,
|
Benefit$inboundSchema,
|
||||||
RunActionCtx,
|
BenefitGrant$inboundSchema,
|
||||||
RunMutationCtx,
|
Product$inboundSchema,
|
||||||
RunQueryCtx,
|
Subscription$inboundSchema,
|
||||||
|
type WebhookBenefitCreatedPayload$Outbound,
|
||||||
|
type WebhookBenefitGrantCreatedPayload$Outbound,
|
||||||
|
type WebhookBenefitGrantUpdatedPayload$Outbound,
|
||||||
|
type WebhookBenefitUpdatedPayload$Outbound,
|
||||||
|
WebhookOrderCreatedPayload$inboundSchema,
|
||||||
|
type WebhookOrderCreatedPayload$Outbound,
|
||||||
|
type WebhookProductCreatedPayload$Outbound,
|
||||||
|
type WebhookProductUpdatedPayload$Outbound,
|
||||||
|
type WebhookSubscriptionCreatedPayload$Outbound,
|
||||||
|
type WebhookSubscriptionUpdatedPayload$Outbound,
|
||||||
|
} from "@polar-sh/sdk/models/components";
|
||||||
|
import {
|
||||||
|
type FunctionReference,
|
||||||
|
type HttpRouter,
|
||||||
|
createFunctionHandle,
|
||||||
|
httpActionGeneric,
|
||||||
|
} from "convex/server";
|
||||||
|
import { Webhook } from "standardwebhooks";
|
||||||
|
import {
|
||||||
|
convertToDatabaseBenefit,
|
||||||
|
convertToDatabaseBenefitGrant,
|
||||||
|
convertToDatabaseOrder,
|
||||||
|
convertToDatabaseProduct,
|
||||||
|
convertToDatabaseSubscription,
|
||||||
|
type ComponentApi,
|
||||||
|
type RunActionCtx,
|
||||||
|
type RunQueryCtx,
|
||||||
} from "../component/util";
|
} from "../component/util";
|
||||||
import { handleWebhook } from "../component/webhook";
|
|
||||||
|
export type EventType = (
|
||||||
|
| WebhookOrderCreatedPayload$Outbound
|
||||||
|
| WebhookSubscriptionCreatedPayload$Outbound
|
||||||
|
| WebhookSubscriptionUpdatedPayload$Outbound
|
||||||
|
| WebhookBenefitCreatedPayload$Outbound
|
||||||
|
| WebhookBenefitUpdatedPayload$Outbound
|
||||||
|
| WebhookProductCreatedPayload$Outbound
|
||||||
|
| WebhookProductUpdatedPayload$Outbound
|
||||||
|
| WebhookBenefitGrantCreatedPayload$Outbound
|
||||||
|
| WebhookBenefitGrantUpdatedPayload$Outbound
|
||||||
|
)["type"];
|
||||||
|
|
||||||
|
export type EventHandler = FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ payload: unknown }
|
||||||
|
>;
|
||||||
|
|
||||||
export class Polar {
|
export class Polar {
|
||||||
public readonly httpPath: string;
|
public readonly httpPath: string;
|
||||||
|
public eventCallback?: EventHandler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public component: ComponentApi,
|
public component: ComponentApi,
|
||||||
options: {
|
options: {
|
||||||
httpPath?: string;
|
httpPath?: string;
|
||||||
|
eventCallback?: EventHandler;
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
|
this.eventCallback = options?.eventCallback;
|
||||||
this.httpPath = options.httpPath ?? "/polar/events";
|
this.httpPath = options.httpPath ?? "/polar/events";
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSubscription(ctx: RunQueryCtx, userId: string) {
|
async listUserSubscriptions(ctx: RunQueryCtx, userId: string) {
|
||||||
const user = await ctx.runQuery(this.component.lib.getUser, { userId });
|
return ctx.runQuery(this.component.lib.listUserSubscriptions, {
|
||||||
return {
|
|
||||||
subscriptionIsPending: user?.subscriptionIsPending,
|
|
||||||
subscription: user?.subscription,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteUserSubscription(ctx: RunMutationCtx, userId: string) {
|
|
||||||
return ctx.runMutation(this.component.lib.deleteUserSubscription, {
|
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async seedProducts(ctx: RunActionCtx) {
|
async listProducts(
|
||||||
return ctx.runAction(this.component.init.seedProducts, {
|
ctx: RunQueryCtx,
|
||||||
|
{ includeArchived = false }: { includeArchived?: boolean } = {}
|
||||||
|
) {
|
||||||
|
return ctx.runQuery(this.component.lib.listPlans, { includeArchived });
|
||||||
|
}
|
||||||
|
|
||||||
|
async pullProducts(ctx: RunActionCtx) {
|
||||||
|
return ctx.runAction(this.component.lib.pullProducts, {
|
||||||
polarAccessToken: process.env.POLAR_ACCESS_TOKEN!,
|
polarAccessToken: process.env.POLAR_ACCESS_TOKEN!,
|
||||||
polarOrganizationId: process.env.POLAR_ORGANIZATION_ID!,
|
polarOrganizationId: process.env.POLAR_ORGANIZATION_ID!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOnboardingCheckoutUrl(
|
|
||||||
ctx: RunActionCtx,
|
|
||||||
args: {
|
|
||||||
successUrl: string;
|
|
||||||
userId: string;
|
|
||||||
userEmail?: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return ctx.runAction(this.component.lib.getOnboardingCheckoutUrl, {
|
|
||||||
successUrl: args.successUrl,
|
|
||||||
userId: args.userId,
|
|
||||||
userEmail: args.userEmail,
|
|
||||||
polarAccessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async getProOnboardingCheckoutUrl(
|
|
||||||
ctx: RunActionCtx,
|
|
||||||
args: {
|
|
||||||
interval: "month" | "year";
|
|
||||||
successUrl: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return ctx.runAction(this.component.lib.getProOnboardingCheckoutUrl, {
|
|
||||||
interval: args.interval,
|
|
||||||
successUrl: args.successUrl,
|
|
||||||
userId: args.userId,
|
|
||||||
polarAccessToken: process.env.POLAR_ACCESS_TOKEN!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async setSubscriptionPending(ctx: RunMutationCtx, userId: string) {
|
|
||||||
return ctx.runMutation(this.component.lib.setSubscriptionPending, {
|
|
||||||
userId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async listPlans(ctx: RunQueryCtx) {
|
|
||||||
return ctx.runQuery(this.component.lib.listPlans);
|
|
||||||
}
|
|
||||||
|
|
||||||
registerRoutes(http: HttpRouter) {
|
registerRoutes(http: HttpRouter) {
|
||||||
http.route({
|
http.route({
|
||||||
path: this.httpPath,
|
path: this.httpPath,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
handler: httpActionGeneric(async (ctx, request) => {
|
handler: httpActionGeneric(async (ctx, request) => {
|
||||||
return handleWebhook(this.component, ctx, request);
|
if (!request.body) {
|
||||||
|
throw new Error("No body");
|
||||||
|
}
|
||||||
|
const body = await request.text();
|
||||||
|
const wh = new Webhook(btoa(process.env.POLAR_WEBHOOK_SECRET!));
|
||||||
|
const headers = Object.fromEntries(request.headers.entries());
|
||||||
|
const payload = wh.verify(body, headers) as {
|
||||||
|
type: EventType;
|
||||||
|
data: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (payload.type) {
|
||||||
|
case "order.created": {
|
||||||
|
await ctx.runMutation(this.component.lib.insertOrder, {
|
||||||
|
order: convertToDatabaseOrder(
|
||||||
|
WebhookOrderCreatedPayload$inboundSchema.parse(payload).data
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "subscription.created": {
|
||||||
|
await ctx.runMutation(this.component.lib.insertSubscription, {
|
||||||
|
subscription: convertToDatabaseSubscription(
|
||||||
|
Subscription$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "subscription.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateSubscription, {
|
||||||
|
subscription: convertToDatabaseSubscription(
|
||||||
|
Subscription$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "product.created": {
|
||||||
|
await ctx.runMutation(this.component.lib.insertProduct, {
|
||||||
|
product: convertToDatabaseProduct(
|
||||||
|
Product$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "product.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateProduct, {
|
||||||
|
product: convertToDatabaseProduct(
|
||||||
|
Product$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "benefit.created": {
|
||||||
|
await ctx.runMutation(this.component.lib.insertBenefit, {
|
||||||
|
benefit: convertToDatabaseBenefit(
|
||||||
|
Benefit$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "benefit.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateBenefit, {
|
||||||
|
benefit: convertToDatabaseBenefit(
|
||||||
|
Benefit$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "benefit_grant.created": {
|
||||||
|
await ctx.runMutation(this.component.lib.insertBenefitGrant, {
|
||||||
|
benefitGrant: convertToDatabaseBenefitGrant(
|
||||||
|
BenefitGrant$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "benefit_grant.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateBenefitGrant, {
|
||||||
|
benefitGrant: convertToDatabaseBenefitGrant(
|
||||||
|
BenefitGrant$inboundSchema.parse(payload.data)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.eventCallback) {
|
||||||
|
await ctx.runMutation(
|
||||||
|
await createFunctionHandle(this.eventCallback),
|
||||||
|
{ payload }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new Response("OK", { status: 200 });
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
673
src/component/_generated/api.d.ts
vendored
673
src/component/_generated/api.d.ts
vendored
@@ -8,12 +8,8 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type * as email_index from "../email/index.js";
|
|
||||||
import type * as email_templates_subscriptionEmail from "../email/templates/subscriptionEmail.js";
|
|
||||||
import type * as init from "../init.js";
|
|
||||||
import type * as lib from "../lib.js";
|
import type * as lib from "../lib.js";
|
||||||
import type * as util from "../util.js";
|
import type * as util from "../util.js";
|
||||||
import type * as webhook from "../webhook.js";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiFromModules,
|
ApiFromModules,
|
||||||
@@ -29,118 +25,613 @@ import type {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
"email/index": typeof email_index;
|
|
||||||
"email/templates/subscriptionEmail": typeof email_templates_subscriptionEmail;
|
|
||||||
init: typeof init;
|
|
||||||
lib: typeof lib;
|
lib: typeof lib;
|
||||||
util: typeof util;
|
util: typeof util;
|
||||||
webhook: typeof webhook;
|
|
||||||
}>;
|
}>;
|
||||||
export type Mounts = {
|
export type Mounts = {
|
||||||
init: {
|
lib: {
|
||||||
seedProducts: FunctionReference<
|
getBenefit: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getBenefitGrant: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getOrder: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getProduct: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
getSubscription: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ id: string },
|
||||||
|
{
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
} | null
|
||||||
|
>;
|
||||||
|
insertBenefit: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
benefit: {
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertBenefitGrant: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
benefitGrant: {
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertOrder: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertProduct: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
insertSubscription: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
subscription: {
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
listBenefits: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
any,
|
||||||
|
Array<{
|
||||||
|
createdAt: string;
|
||||||
|
deletable: boolean;
|
||||||
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listPlans: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ includeArchived: boolean },
|
||||||
|
Array<{
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listUserBenefitGrants: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ userId: string },
|
||||||
|
Array<{
|
||||||
|
benefitId: string;
|
||||||
|
createdAt: string;
|
||||||
|
grantedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
listUserSubscriptions: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"public",
|
||||||
|
{ userId: string },
|
||||||
|
Array<{
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceId: string;
|
||||||
|
product?: {
|
||||||
|
_creationTime: number;
|
||||||
|
_id: string;
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
|
status: string;
|
||||||
|
userId: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
pullProducts: FunctionReference<
|
||||||
"action",
|
"action",
|
||||||
"public",
|
"public",
|
||||||
{ polarAccessToken: string; polarOrganizationId: string },
|
{ polarAccessToken: string; polarOrganizationId: string },
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
};
|
updateBenefit: FunctionReference<
|
||||||
lib: {
|
|
||||||
createUser: FunctionReference<
|
|
||||||
"mutation",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
deleteUserSubscription: FunctionReference<
|
|
||||||
"mutation",
|
|
||||||
"public",
|
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getOnboardingCheckoutUrl: FunctionReference<
|
|
||||||
"action",
|
|
||||||
"public",
|
|
||||||
{
|
{
|
||||||
polarAccessToken: string;
|
benefit: {
|
||||||
successUrl: string;
|
createdAt: string;
|
||||||
userEmail?: string;
|
deletable: boolean;
|
||||||
userId: string;
|
description: string;
|
||||||
|
id: string;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
organizationId: string;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
selectable: boolean;
|
||||||
|
type?: string;
|
||||||
|
};
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
getPlanByKey: FunctionReference<
|
updateBenefitGrant: FunctionReference<
|
||||||
"query",
|
|
||||||
"public",
|
|
||||||
{ key: "free" | "pro" },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getProOnboardingCheckoutUrl: FunctionReference<
|
|
||||||
"action",
|
|
||||||
"public",
|
|
||||||
{
|
|
||||||
interval: "month" | "year";
|
|
||||||
polarAccessToken: string;
|
|
||||||
successUrl: string;
|
|
||||||
userId: string;
|
|
||||||
},
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
getUser: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"public",
|
|
||||||
{ userId: string },
|
|
||||||
null | {
|
|
||||||
polarId?: string;
|
|
||||||
subscription?: {
|
|
||||||
cancelAtPeriodEnd?: boolean;
|
|
||||||
currency: "usd" | "eur";
|
|
||||||
currentPeriodEnd?: number;
|
|
||||||
currentPeriodStart?: number;
|
|
||||||
interval: "month" | "year";
|
|
||||||
localUserId: string;
|
|
||||||
planId: string;
|
|
||||||
polarId: string;
|
|
||||||
polarPriceId: string;
|
|
||||||
status: string;
|
|
||||||
};
|
|
||||||
subscriptionIsPending?: boolean;
|
|
||||||
subscriptionPendingId?: string;
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
getUserByLocalId: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"public",
|
|
||||||
{ localUserId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
listPlans: FunctionReference<"query", "public", {}, any>;
|
|
||||||
replaceSubscription: FunctionReference<
|
|
||||||
"mutation",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
{
|
{
|
||||||
input: {
|
benefitGrant: {
|
||||||
cancelAtPeriodEnd?: boolean;
|
benefitId: string;
|
||||||
currency: "usd" | "eur";
|
createdAt: string;
|
||||||
currentPeriodEnd?: number;
|
grantedAt: string | null;
|
||||||
currentPeriodStart: number;
|
id: string;
|
||||||
interval: "month" | "year";
|
isGranted: boolean;
|
||||||
|
isRevoked: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
orderId: string | null;
|
||||||
|
properties: Record<string, any>;
|
||||||
|
revokedAt: string | null;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
userId: string;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateOrder: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
order: {
|
||||||
|
amount: number;
|
||||||
|
billingReason: string;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
productId: string | null;
|
||||||
|
productPriceId: string;
|
||||||
|
subscriptionId: string | null;
|
||||||
|
taxAmount: number;
|
||||||
|
userId: string | null;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateProduct: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
product: {
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateProducts: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
polarAccessToken: string;
|
||||||
|
products: Array<{
|
||||||
|
createdAt: string;
|
||||||
|
description: string | null;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
isRecurring: boolean;
|
||||||
|
medias: Array<{
|
||||||
|
checksumEtag: string | null;
|
||||||
|
checksumSha256Base64: string | null;
|
||||||
|
checksumSha256Hex: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isUploaded: boolean;
|
||||||
|
lastModifiedAt: string | null;
|
||||||
|
mimeType: string;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
path: string;
|
||||||
|
publicUrl: string;
|
||||||
|
service?: string;
|
||||||
|
size: number;
|
||||||
|
sizeReadable: string;
|
||||||
|
storageVersion: string | null;
|
||||||
|
version: string | null;
|
||||||
|
}>;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
name: string;
|
||||||
|
organizationId: string;
|
||||||
|
prices: Array<{
|
||||||
|
amountType?: string;
|
||||||
|
createdAt: string;
|
||||||
|
id: string;
|
||||||
|
isArchived: boolean;
|
||||||
|
modifiedAt: string | null;
|
||||||
|
priceAmount?: number;
|
||||||
|
priceCurrency?: string;
|
||||||
|
productId: string;
|
||||||
|
recurringInterval?: string;
|
||||||
|
type?: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
},
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
updateSubscription: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"public",
|
||||||
|
{
|
||||||
|
subscription: {
|
||||||
|
amount: number | null;
|
||||||
|
cancelAtPeriodEnd: boolean;
|
||||||
|
checkoutId: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
currency: string | null;
|
||||||
|
currentPeriodEnd: string | null;
|
||||||
|
currentPeriodStart: string;
|
||||||
|
endedAt: string | null;
|
||||||
|
id: string;
|
||||||
|
metadata: Record<string, any>;
|
||||||
|
modifiedAt: string | null;
|
||||||
priceId: string;
|
priceId: string;
|
||||||
productId: string;
|
productId: string;
|
||||||
|
recurringInterval: string;
|
||||||
|
startedAt: string | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
userId: string;
|
||||||
};
|
};
|
||||||
localUserId: string;
|
|
||||||
subscriptionPolarId: string;
|
|
||||||
},
|
},
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
setSubscriptionPending: FunctionReference<
|
|
||||||
"mutation",
|
|
||||||
"public",
|
|
||||||
{ userId: string },
|
|
||||||
any
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
// For now fullApiWithMounts is only fullApi which provides
|
// For now fullApiWithMounts is only fullApi which provides
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const ResendSuccessSchema = z.object({
|
|
||||||
id: z.string(),
|
|
||||||
});
|
|
||||||
const ResendErrorSchema = z.union([
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
message: z.string(),
|
|
||||||
statusCode: z.number(),
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
name: z.literal("UnknownError"),
|
|
||||||
message: z.literal("Unknown Error"),
|
|
||||||
statusCode: z.literal(500),
|
|
||||||
cause: z.any(),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export type SendEmailOptions = {
|
|
||||||
to: string | string[];
|
|
||||||
subject: string;
|
|
||||||
html: string;
|
|
||||||
text?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function sendEmail(options: SendEmailOptions) {
|
|
||||||
const from =
|
|
||||||
process.env.RESEND_SENDER_EMAIL_AUTH ??
|
|
||||||
"Convex SaaS <onboarding@resend.dev>";
|
|
||||||
const email = { from, ...options };
|
|
||||||
|
|
||||||
const response = await fetch("https://api.resend.com/emails", {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${process.env.RESEND_API_KEY}`,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(email),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const parsedData = ResendSuccessSchema.safeParse(data);
|
|
||||||
|
|
||||||
if (response.ok && parsedData.success) {
|
|
||||||
return { status: "success", data: parsedData } as const;
|
|
||||||
}
|
|
||||||
const parsedErrorResult = ResendErrorSchema.safeParse(data);
|
|
||||||
if (parsedErrorResult.success) {
|
|
||||||
console.error(parsedErrorResult.data);
|
|
||||||
throw new Error(`Error sending email: ${parsedErrorResult.data.message}`);
|
|
||||||
}
|
|
||||||
console.error(data);
|
|
||||||
throw new Error("Error sending email");
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import {
|
|
||||||
Body,
|
|
||||||
Container,
|
|
||||||
Head,
|
|
||||||
Hr,
|
|
||||||
Html,
|
|
||||||
Img,
|
|
||||||
Link,
|
|
||||||
Preview,
|
|
||||||
Text,
|
|
||||||
} from "@react-email/components";
|
|
||||||
import { render } from "@react-email/render";
|
|
||||||
import { sendEmail } from "../index";
|
|
||||||
|
|
||||||
type SubscriptionEmailOptions = {
|
|
||||||
email: string;
|
|
||||||
subscriptionId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Templates.
|
|
||||||
*/
|
|
||||||
export function SubscriptionSuccessEmail({ email }: SubscriptionEmailOptions) {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<Preview>Successfully Subscribed to PRO</Preview>
|
|
||||||
<Body
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
fontFamily:
|
|
||||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container style={{ margin: "0 auto", padding: "20px 0 48px" }}>
|
|
||||||
<Img
|
|
||||||
src={`${process.env.SITE_URL}/images/convex-logo-email.jpg`}
|
|
||||||
width="40"
|
|
||||||
height="37"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
Hello {email}!
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
Your subscription to PRO has been successfully processed.
|
|
||||||
<br />
|
|
||||||
We hope you enjoy the new features!
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
The <Link href={`${process.env.SITE_URL}`}>domain-name.com</Link>{" "}
|
|
||||||
team.
|
|
||||||
</Text>
|
|
||||||
<Hr style={{ borderColor: "#cccccc", margin: "20px 0" }} />
|
|
||||||
<Text style={{ color: "#8898aa", fontSize: "12px" }}>
|
|
||||||
200 domain-name.com
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SubscriptionErrorEmail({ email }: SubscriptionEmailOptions) {
|
|
||||||
return (
|
|
||||||
<Html>
|
|
||||||
<Head />
|
|
||||||
<Preview>Subscription Issue - Customer Support</Preview>
|
|
||||||
<Body
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
fontFamily:
|
|
||||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Container style={{ margin: "0 auto", padding: "20px 0 48px" }}>
|
|
||||||
<Img
|
|
||||||
src="https://react-email-demo-ijnnx5hul-resend.vercel.app/static/vercel-logo.png"
|
|
||||||
width="40"
|
|
||||||
height="37"
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
Hello {email}.
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
We were unable to process your subscription to PRO tier.
|
|
||||||
<br />
|
|
||||||
But don't worry, we'll not charge you anything.
|
|
||||||
</Text>
|
|
||||||
<Text style={{ fontSize: "16px", lineHeight: "26px" }}>
|
|
||||||
The <Link href={`${process.env.SITE_URL}`}>domain-name.com</Link>{" "}
|
|
||||||
team.
|
|
||||||
</Text>
|
|
||||||
<Hr style={{ borderColor: "#cccccc", margin: "20px 0" }} />
|
|
||||||
<Text style={{ color: "#8898aa", fontSize: "12px" }}>
|
|
||||||
200 domain-name.com
|
|
||||||
</Text>
|
|
||||||
</Container>
|
|
||||||
</Body>
|
|
||||||
</Html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders.
|
|
||||||
*/
|
|
||||||
export function renderSubscriptionSuccessEmail(args: SubscriptionEmailOptions) {
|
|
||||||
return render(<SubscriptionSuccessEmail {...args} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function renderSubscriptionErrorEmail(args: SubscriptionEmailOptions) {
|
|
||||||
return render(<SubscriptionErrorEmail {...args} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Senders.
|
|
||||||
*/
|
|
||||||
export async function sendSubscriptionSuccessEmail({
|
|
||||||
email,
|
|
||||||
subscriptionId,
|
|
||||||
}: SubscriptionEmailOptions) {
|
|
||||||
const html = await renderSubscriptionSuccessEmail({ email, subscriptionId });
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
to: email,
|
|
||||||
subject: "Successfully Subscribed to PRO",
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function sendSubscriptionErrorEmail({
|
|
||||||
email,
|
|
||||||
subscriptionId,
|
|
||||||
}: SubscriptionEmailOptions) {
|
|
||||||
const html = await renderSubscriptionErrorEmail({ email, subscriptionId });
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
to: email,
|
|
||||||
subject: "Subscription Issue - Customer Support",
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { Polar } from "@polar-sh/sdk";
|
|
||||||
import { asyncMap } from "convex-helpers";
|
|
||||||
import { internal } from "./_generated/api";
|
|
||||||
import { action, internalMutation } from "./_generated/server";
|
|
||||||
import schema, { CURRENCIES, INTERVALS, PlanKey, PLANS } from "./schema";
|
|
||||||
import { v } from "convex/values";
|
|
||||||
|
|
||||||
const seedProducts = [
|
|
||||||
{
|
|
||||||
key: PLANS.FREE,
|
|
||||||
name: "Free",
|
|
||||||
description: "Some of the things, free forever.",
|
|
||||||
amountType: "free",
|
|
||||||
prices: {
|
|
||||||
[INTERVALS.MONTH]: {
|
|
||||||
[CURRENCIES.USD]: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: PLANS.PRO,
|
|
||||||
name: "Pro",
|
|
||||||
description: "All the things for one low monthly price.",
|
|
||||||
amountType: "fixed",
|
|
||||||
prices: {
|
|
||||||
[INTERVALS.MONTH]: {
|
|
||||||
[CURRENCIES.USD]: 2000,
|
|
||||||
},
|
|
||||||
[INTERVALS.YEAR]: {
|
|
||||||
[CURRENCIES.USD]: 20000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const insertSeedPlan = internalMutation({
|
|
||||||
args: schema.tables.plans.validator,
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
await ctx.db.insert("plans", {
|
|
||||||
polarProductId: args.polarProductId,
|
|
||||||
key: args.key,
|
|
||||||
name: args.name,
|
|
||||||
description: args.description,
|
|
||||||
prices: args.prices,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const seedProductsAction = action({
|
|
||||||
args: {
|
|
||||||
polarAccessToken: v.string(),
|
|
||||||
polarOrganizationId: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
/**
|
|
||||||
* Stripe Products.
|
|
||||||
*/
|
|
||||||
const polar = new Polar({
|
|
||||||
server: "sandbox",
|
|
||||||
accessToken: args.polarAccessToken,
|
|
||||||
});
|
|
||||||
const products = await polar.products.list({
|
|
||||||
organizationId: args.polarOrganizationId,
|
|
||||||
isArchived: false,
|
|
||||||
});
|
|
||||||
if (products?.result?.items?.length) {
|
|
||||||
console.info("🏃♂️ Skipping Polar products creation and seeding.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await asyncMap(seedProducts, async (product) => {
|
|
||||||
// Create Polar product.
|
|
||||||
const polarProduct = await polar.products.create({
|
|
||||||
organizationId: args.polarOrganizationId,
|
|
||||||
name: product.name,
|
|
||||||
description: product.description,
|
|
||||||
prices: Object.entries(product.prices).map(([interval, amount]) => ({
|
|
||||||
amountType: product.amountType,
|
|
||||||
priceAmount: amount.usd,
|
|
||||||
recurringInterval: interval,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
const monthPrice = polarProduct.prices.find(
|
|
||||||
(price) =>
|
|
||||||
price.type === "recurring" &&
|
|
||||||
price.recurringInterval === INTERVALS.MONTH
|
|
||||||
);
|
|
||||||
const yearPrice = polarProduct.prices.find(
|
|
||||||
(price) =>
|
|
||||||
price.type === "recurring" &&
|
|
||||||
price.recurringInterval === INTERVALS.YEAR
|
|
||||||
);
|
|
||||||
|
|
||||||
await ctx.runMutation(internal.init.insertSeedPlan, {
|
|
||||||
polarProductId: polarProduct.id,
|
|
||||||
key: product.key as PlanKey,
|
|
||||||
name: product.name,
|
|
||||||
description: product.description,
|
|
||||||
prices: {
|
|
||||||
...(!monthPrice
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
month: {
|
|
||||||
usd: {
|
|
||||||
polarId: monthPrice?.id,
|
|
||||||
amount:
|
|
||||||
monthPrice.amountType === "fixed"
|
|
||||||
? monthPrice.priceAmount
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(!yearPrice
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
year: {
|
|
||||||
usd: {
|
|
||||||
polarId: yearPrice?.id,
|
|
||||||
amount:
|
|
||||||
yearPrice.amountType === "fixed"
|
|
||||||
? yearPrice.priceAmount
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.info("📦 Polar Products have been successfully created.");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { seedProductsAction as seedProducts };
|
|
||||||
@@ -1,320 +1,323 @@
|
|||||||
import { Polar } from "@polar-sh/sdk";
|
import { Polar } from "@polar-sh/sdk";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { api, internal } from "./_generated/api";
|
import { api } from "./_generated/api";
|
||||||
import type { Id } from "./_generated/dataModel";
|
import { action, mutation, query } from "./_generated/server";
|
||||||
import {
|
|
||||||
action,
|
|
||||||
internalMutation,
|
|
||||||
mutation,
|
|
||||||
query,
|
|
||||||
QueryCtx,
|
|
||||||
} from "./_generated/server";
|
|
||||||
import schema from "./schema";
|
import schema from "./schema";
|
||||||
|
import { asyncMap } from "convex-helpers";
|
||||||
|
import { convertToDatabaseProduct } from "./util";
|
||||||
|
|
||||||
const createCheckout = async ({
|
export const getSubscription = query({
|
||||||
polarAccessToken,
|
|
||||||
customerEmail,
|
|
||||||
productPriceId,
|
|
||||||
successUrl,
|
|
||||||
polarSubscriptionId,
|
|
||||||
localUserId,
|
|
||||||
}: {
|
|
||||||
polarAccessToken: string;
|
|
||||||
customerEmail?: string;
|
|
||||||
productPriceId: string;
|
|
||||||
successUrl: string;
|
|
||||||
polarSubscriptionId?: string;
|
|
||||||
localUserId: Id<"users">;
|
|
||||||
}) => {
|
|
||||||
const polar = new Polar({
|
|
||||||
server: "sandbox",
|
|
||||||
accessToken: polarAccessToken,
|
|
||||||
});
|
|
||||||
if (polarSubscriptionId) {
|
|
||||||
return polar.checkouts.create({
|
|
||||||
productPriceId,
|
|
||||||
successUrl,
|
|
||||||
subscriptionId: polarSubscriptionId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return polar.checkouts.custom.create({
|
|
||||||
productPriceId,
|
|
||||||
successUrl,
|
|
||||||
customerEmail,
|
|
||||||
metadata: {
|
|
||||||
userId: localUserId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPlanByKey = query({
|
|
||||||
args: {
|
args: {
|
||||||
key: schema.tables.plans.validator.fields.key,
|
id: v.id("subscriptions"),
|
||||||
},
|
},
|
||||||
|
returns: v.union(schema.tables.subscriptions.validator, v.null()),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
return ctx.db
|
return ctx.db
|
||||||
.query("plans")
|
.query("subscriptions")
|
||||||
.withIndex("key", (q) => q.eq("key", args.key))
|
.withIndex("id", (q) => q.eq("id", args.id))
|
||||||
.unique();
|
.unique();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createUser = mutation({
|
export const getOrder = query({
|
||||||
args: {
|
args: {
|
||||||
userId: v.string(),
|
id: v.id("orders"),
|
||||||
},
|
},
|
||||||
|
returns: v.union(schema.tables.orders.validator, v.null()),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const userId = await ctx.db.insert("users", {
|
return ctx.db
|
||||||
userId: args.userId,
|
.query("orders")
|
||||||
});
|
.withIndex("id", (q) => q.eq("id", args.id))
|
||||||
const user = await ctx.db.get(userId);
|
.unique();
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const getUser = async (ctx: QueryCtx, localUserId: Id<"users">) => {
|
export const getProduct = query({
|
||||||
const user = await ctx.db.get(localUserId);
|
args: {
|
||||||
if (!user) {
|
id: v.id("products"),
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const { subscriptionPendingId, ...subscriptionUser } = user;
|
|
||||||
const subscription =
|
|
||||||
(await ctx.db
|
|
||||||
.query("subscriptions")
|
|
||||||
.withIndex("localUserId", (q) => q.eq("localUserId", user._id))
|
|
||||||
.unique()) || undefined;
|
|
||||||
const plan = subscription ? await ctx.db.get(subscription.planId) : undefined;
|
|
||||||
return {
|
|
||||||
...subscriptionUser,
|
|
||||||
subscriptionIsPending: !!subscriptionPendingId,
|
|
||||||
...(subscription && {
|
|
||||||
subscription: {
|
|
||||||
...subscription,
|
|
||||||
plan,
|
|
||||||
},
|
},
|
||||||
}),
|
returns: v.union(schema.tables.products.validator, v.null()),
|
||||||
};
|
handler: async (ctx, args) => {
|
||||||
};
|
return ctx.db
|
||||||
|
.query("products")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.id))
|
||||||
|
.unique();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const getUserQuery = query({
|
export const listUserSubscriptions = query({
|
||||||
args: {
|
args: {
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
},
|
},
|
||||||
returns: v.union(
|
returns: v.array(
|
||||||
v.null(),
|
|
||||||
v.object({
|
v.object({
|
||||||
...schema.tables.users.validator.fields,
|
...schema.tables.subscriptions.validator.fields,
|
||||||
subscriptionIsPending: v.optional(v.boolean()),
|
_id: v.id("subscriptions"),
|
||||||
subscription: v.optional(schema.tables.subscriptions.validator),
|
_creationTime: v.number(),
|
||||||
|
product: v.optional(
|
||||||
|
v.object({
|
||||||
|
...schema.tables.products.validator.fields,
|
||||||
|
_id: v.id("products"),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
})
|
||||||
|
),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await ctx.db
|
return asyncMap(
|
||||||
.query("users")
|
ctx.db
|
||||||
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
|
||||||
.unique();
|
|
||||||
if (!user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return getUser(ctx, user._id);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export { getUserQuery as getUser };
|
|
||||||
|
|
||||||
export const getUserByLocalId = query({
|
|
||||||
args: {
|
|
||||||
localUserId: v.id("users"),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => getUser(ctx, args.localUserId),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteUserSubscription = mutation({
|
|
||||||
args: {
|
|
||||||
userId: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const user = await ctx.db
|
|
||||||
.query("users")
|
|
||||||
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
|
||||||
.unique();
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
const subscription = await ctx.db
|
|
||||||
.query("subscriptions")
|
.query("subscriptions")
|
||||||
.withIndex("localUserId", (q) => q.eq("localUserId", user._id))
|
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
||||||
.unique();
|
.collect(),
|
||||||
if (subscription) {
|
async (subscription) => {
|
||||||
await ctx.db.delete(subscription._id);
|
const product = subscription.productId
|
||||||
|
? (await ctx.db
|
||||||
|
.query("products")
|
||||||
|
.withIndex("id", (q) => q.eq("id", subscription.productId))
|
||||||
|
.unique()) || undefined
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
...subscription,
|
||||||
|
product,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
});
|
|
||||||
|
|
||||||
export const getOnboardingCheckoutUrl = action({
|
|
||||||
args: {
|
|
||||||
successUrl: v.string(),
|
|
||||||
userId: v.string(),
|
|
||||||
userEmail: v.optional(v.string()),
|
|
||||||
polarAccessToken: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const user =
|
|
||||||
(await ctx.runQuery(api.lib.getUser, {
|
|
||||||
userId: args.userId,
|
|
||||||
})) ||
|
|
||||||
(await ctx.runMutation(api.lib.createUser, {
|
|
||||||
userId: args.userId,
|
|
||||||
}));
|
|
||||||
const product = await ctx.runQuery(api.lib.getPlanByKey, {
|
|
||||||
key: "free",
|
|
||||||
});
|
|
||||||
const price = product?.prices.month?.usd;
|
|
||||||
if (!price) {
|
|
||||||
throw new Error("Price not found");
|
|
||||||
}
|
|
||||||
const checkout = await createCheckout({
|
|
||||||
polarAccessToken: args.polarAccessToken,
|
|
||||||
customerEmail: args.userEmail,
|
|
||||||
productPriceId: price.polarId,
|
|
||||||
successUrl: args.successUrl,
|
|
||||||
localUserId: user?._id,
|
|
||||||
});
|
|
||||||
return checkout.url;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getProOnboardingCheckoutUrl = action({
|
|
||||||
args: {
|
|
||||||
interval: schema.tables.subscriptions.validator.fields.interval,
|
|
||||||
polarAccessToken: v.string(),
|
|
||||||
successUrl: v.string(),
|
|
||||||
userId: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
const product = await ctx.runQuery(api.lib.getPlanByKey, {
|
|
||||||
key: "pro",
|
|
||||||
});
|
|
||||||
const price =
|
|
||||||
args.interval === "month"
|
|
||||||
? product?.prices.month?.usd
|
|
||||||
: product?.prices.year?.usd;
|
|
||||||
if (!price) {
|
|
||||||
throw new Error("Price not found");
|
|
||||||
}
|
|
||||||
const user = await ctx.runQuery(api.lib.getUser, {
|
|
||||||
userId: args.userId,
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
const checkout = await createCheckout({
|
|
||||||
polarAccessToken: args.polarAccessToken,
|
|
||||||
productPriceId: price.polarId,
|
|
||||||
successUrl: args.successUrl,
|
|
||||||
polarSubscriptionId: user?.subscription?.polarId,
|
|
||||||
localUserId: user?._id,
|
|
||||||
});
|
|
||||||
return checkout.url;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const listPlans = query({
|
export const listPlans = query({
|
||||||
args: {},
|
args: {
|
||||||
handler: async (ctx) => {
|
includeArchived: v.boolean(),
|
||||||
const plans = await ctx.db.query("plans").collect();
|
},
|
||||||
return plans.sort((a, b) => a.key.localeCompare(b.key));
|
returns: v.array(
|
||||||
|
v.object({
|
||||||
|
...schema.tables.products.validator.fields,
|
||||||
|
_id: v.id("products"),
|
||||||
|
_creationTime: v.number(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
if (args.includeArchived) {
|
||||||
|
return ctx.db.query("products").collect();
|
||||||
|
}
|
||||||
|
return ctx.db
|
||||||
|
.query("products")
|
||||||
|
.withIndex("isArchived", (q) => q.lt("isArchived", true))
|
||||||
|
.collect();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const replaceSubscription = mutation({
|
export const insertOrder = mutation({
|
||||||
args: {
|
args: {
|
||||||
localUserId: v.id("users"),
|
order: schema.tables.orders.validator,
|
||||||
subscriptionPolarId: v.string(),
|
|
||||||
input: v.object({
|
|
||||||
currency: schema.tables.subscriptions.validator.fields.currency,
|
|
||||||
productId: v.string(),
|
|
||||||
priceId: v.string(),
|
|
||||||
interval: schema.tables.subscriptions.validator.fields.interval,
|
|
||||||
status: v.string(),
|
|
||||||
currentPeriodStart: v.number(),
|
|
||||||
currentPeriodEnd: v.optional(v.number()),
|
|
||||||
cancelAtPeriodEnd: v.optional(v.boolean()),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const subscription = await ctx.db
|
await ctx.db.insert("orders", args.order);
|
||||||
.query("subscriptions")
|
},
|
||||||
.withIndex("localUserId", (q) => q.eq("localUserId", args.localUserId))
|
|
||||||
.unique();
|
|
||||||
if (subscription) {
|
|
||||||
await ctx.db.delete(subscription._id);
|
|
||||||
}
|
|
||||||
const plan = await ctx.db
|
|
||||||
.query("plans")
|
|
||||||
.withIndex("polarProductId", (q) =>
|
|
||||||
q.eq("polarProductId", args.input.productId)
|
|
||||||
)
|
|
||||||
.unique();
|
|
||||||
if (!plan) {
|
|
||||||
throw new Error("Plan not found");
|
|
||||||
}
|
|
||||||
await ctx.db.insert("subscriptions", {
|
|
||||||
localUserId: args.localUserId,
|
|
||||||
planId: plan._id,
|
|
||||||
polarId: args.subscriptionPolarId,
|
|
||||||
polarPriceId: args.input.priceId,
|
|
||||||
interval: args.input.interval,
|
|
||||||
status: args.input.status,
|
|
||||||
currency: args.input.currency,
|
|
||||||
currentPeriodStart: args.input.currentPeriodStart,
|
|
||||||
currentPeriodEnd: args.input.currentPeriodEnd,
|
|
||||||
cancelAtPeriodEnd: args.input.cancelAtPeriodEnd,
|
|
||||||
});
|
});
|
||||||
const user = await ctx.db.get(args.localUserId);
|
|
||||||
if (!user?.subscriptionPendingId) {
|
export const updateOrder = mutation({
|
||||||
|
args: {
|
||||||
|
order: schema.tables.orders.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existingOrder = await ctx.db
|
||||||
|
.query("orders")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.order.id))
|
||||||
|
.unique();
|
||||||
|
if (existingOrder) {
|
||||||
|
await ctx.db.patch(existingOrder._id, args.order);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertSubscription = mutation({
|
||||||
|
args: {
|
||||||
|
subscription: schema.tables.subscriptions.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("subscriptions", args.subscription);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSubscription = mutation({
|
||||||
|
args: {
|
||||||
|
subscription: schema.tables.subscriptions.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existingSubscription = await ctx.db
|
||||||
|
.query("subscriptions")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.subscription.id))
|
||||||
|
.unique();
|
||||||
|
if (existingSubscription) {
|
||||||
|
await ctx.db.patch(existingSubscription._id, args.subscription);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertProduct = mutation({
|
||||||
|
args: {
|
||||||
|
product: schema.tables.products.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("products", args.product);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProduct = mutation({
|
||||||
|
args: {
|
||||||
|
product: schema.tables.products.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existingProduct = await ctx.db
|
||||||
|
.query("products")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.product.id))
|
||||||
|
.unique();
|
||||||
|
if (existingProduct) {
|
||||||
|
await ctx.db.patch(existingProduct._id, args.product);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateProducts = mutation({
|
||||||
|
args: {
|
||||||
|
polarAccessToken: v.string(),
|
||||||
|
products: v.array(schema.tables.products.validator),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await asyncMap(args.products, async (product) => {
|
||||||
|
console.log(product);
|
||||||
|
const existingProduct = await ctx.db
|
||||||
|
.query("products")
|
||||||
|
.withIndex("id", (q) => q.eq("id", product.id))
|
||||||
|
.unique();
|
||||||
|
if (existingProduct) {
|
||||||
|
await ctx.db.patch(existingProduct._id, product);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await ctx.scheduler.cancel(user.subscriptionPendingId);
|
await ctx.db.insert("products", product);
|
||||||
await ctx.db.patch(args.localUserId, {
|
|
||||||
subscriptionPendingId: undefined,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setSubscriptionPending = mutation({
|
export const pullProducts = action({
|
||||||
|
args: {
|
||||||
|
polarAccessToken: v.string(),
|
||||||
|
polarOrganizationId: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const polar = new Polar({
|
||||||
|
server: "sandbox",
|
||||||
|
accessToken: args.polarAccessToken,
|
||||||
|
});
|
||||||
|
let page = 1;
|
||||||
|
let maxPage;
|
||||||
|
do {
|
||||||
|
const products = await polar.products.list({
|
||||||
|
page,
|
||||||
|
limit: 10,
|
||||||
|
organizationId: args.polarOrganizationId,
|
||||||
|
});
|
||||||
|
page = page + 1;
|
||||||
|
maxPage = products.result.pagination.maxPage;
|
||||||
|
await ctx.runMutation(api.lib.updateProducts, {
|
||||||
|
polarAccessToken: args.polarAccessToken,
|
||||||
|
products: products.result.items.map(convertToDatabaseProduct),
|
||||||
|
});
|
||||||
|
} while (maxPage >= page);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertBenefit = mutation({
|
||||||
|
args: {
|
||||||
|
benefit: schema.tables.benefits.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("benefits", args.benefit);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateBenefit = mutation({
|
||||||
|
args: {
|
||||||
|
benefit: schema.tables.benefits.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existingBenefit = await ctx.db
|
||||||
|
.query("benefits")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.benefit.id))
|
||||||
|
.unique();
|
||||||
|
if (existingBenefit) {
|
||||||
|
await ctx.db.patch(existingBenefit._id, args.benefit);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getBenefit = query({
|
||||||
|
args: {
|
||||||
|
id: v.id("benefits"),
|
||||||
|
},
|
||||||
|
returns: v.union(schema.tables.benefits.validator, v.null()),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.db
|
||||||
|
.query("benefits")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.id))
|
||||||
|
.unique();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listBenefits = query({
|
||||||
|
returns: v.array(schema.tables.benefits.validator),
|
||||||
|
handler: async (ctx, _args) => {
|
||||||
|
return ctx.db.query("benefits").collect();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertBenefitGrant = mutation({
|
||||||
|
args: {
|
||||||
|
benefitGrant: schema.tables.benefitGrants.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("benefitGrants", args.benefitGrant);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateBenefitGrant = mutation({
|
||||||
|
args: {
|
||||||
|
benefitGrant: schema.tables.benefitGrants.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existingBenefitGrant = await ctx.db
|
||||||
|
.query("benefitGrants")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.benefitGrant.id))
|
||||||
|
.unique();
|
||||||
|
if (existingBenefitGrant) {
|
||||||
|
await ctx.db.patch(existingBenefitGrant._id, args.benefitGrant);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getBenefitGrant = query({
|
||||||
|
args: {
|
||||||
|
id: v.id("benefitGrants"),
|
||||||
|
},
|
||||||
|
returns: v.union(schema.tables.benefitGrants.validator, v.null()),
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
return ctx.db
|
||||||
|
.query("benefitGrants")
|
||||||
|
.withIndex("id", (q) => q.eq("id", args.id))
|
||||||
|
.unique();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const listUserBenefitGrants = query({
|
||||||
args: {
|
args: {
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
},
|
},
|
||||||
|
returns: v.array(schema.tables.benefitGrants.validator),
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const user = await ctx.db
|
return ctx.db
|
||||||
.query("users")
|
.query("benefitGrants")
|
||||||
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
||||||
.unique();
|
.collect();
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
const scheduledFunctionId = await ctx.scheduler.runAfter(
|
|
||||||
1000 * 120,
|
|
||||||
internal.lib.unsetSubscriptionPending,
|
|
||||||
{ localUserId: user._id }
|
|
||||||
);
|
|
||||||
await ctx.db.patch(user._id, {
|
|
||||||
subscriptionPendingId: scheduledFunctionId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const unsetSubscriptionPending = internalMutation({
|
|
||||||
args: {
|
|
||||||
localUserId: v.id("users"),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
await ctx.db.patch(args.localUserId, {
|
|
||||||
subscriptionPendingId: undefined,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
import { defineSchema, defineTable } from "convex/server";
|
import { defineSchema, defineTable } from "convex/server";
|
||||||
import { Infer, v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
|
|
||||||
export const CURRENCIES = {
|
|
||||||
USD: "usd",
|
|
||||||
EUR: "eur",
|
|
||||||
} as const;
|
|
||||||
export const currencyValidator = v.union(
|
|
||||||
v.literal(CURRENCIES.USD),
|
|
||||||
v.literal(CURRENCIES.EUR)
|
|
||||||
);
|
|
||||||
|
|
||||||
export const INTERVALS = {
|
export const INTERVALS = {
|
||||||
MONTH: "month",
|
MONTH: "month",
|
||||||
@@ -19,58 +10,129 @@ export const intervalValidator = v.union(
|
|||||||
v.literal(INTERVALS.YEAR)
|
v.literal(INTERVALS.YEAR)
|
||||||
);
|
);
|
||||||
|
|
||||||
const priceValidator = v.object({
|
export default defineSchema(
|
||||||
polarId: v.string(),
|
{
|
||||||
amount: v.number(),
|
|
||||||
});
|
|
||||||
const pricesValidator = v.object({
|
|
||||||
[CURRENCIES.USD]: v.optional(priceValidator),
|
|
||||||
[CURRENCIES.EUR]: v.optional(priceValidator),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const PLANS = {
|
|
||||||
FREE: "free",
|
|
||||||
PRO: "pro",
|
|
||||||
} as const;
|
|
||||||
export const planKeyValidator = v.union(
|
|
||||||
v.literal(PLANS.FREE),
|
|
||||||
v.literal(PLANS.PRO)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type PlanKey = Infer<typeof planKeyValidator>;
|
|
||||||
|
|
||||||
export default defineSchema({
|
|
||||||
users: defineTable({
|
users: defineTable({
|
||||||
|
id: v.string(),
|
||||||
userId: v.string(),
|
userId: v.string(),
|
||||||
polarId: v.optional(v.string()),
|
|
||||||
subscriptionPendingId: v.optional(v.id("_scheduled_functions")),
|
|
||||||
})
|
})
|
||||||
.index("userId", ["userId"])
|
.index("id", ["id"])
|
||||||
.index("polarId", ["polarId"]),
|
.index("userId", ["userId"]),
|
||||||
plans: defineTable({
|
benefits: defineTable({
|
||||||
key: planKeyValidator,
|
id: v.string(),
|
||||||
polarProductId: v.string(),
|
createdAt: v.string(),
|
||||||
name: v.string(),
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
|
organizationId: v.string(),
|
||||||
|
type: v.optional(v.string()),
|
||||||
description: v.string(),
|
description: v.string(),
|
||||||
prices: v.object({
|
selectable: v.boolean(),
|
||||||
[INTERVALS.MONTH]: v.optional(pricesValidator),
|
deletable: v.boolean(),
|
||||||
[INTERVALS.YEAR]: v.optional(pricesValidator),
|
properties: v.record(v.string(), v.any()),
|
||||||
}),
|
}).index("id", ["id"]),
|
||||||
|
benefitGrants: defineTable({
|
||||||
|
id: v.string(),
|
||||||
|
createdAt: v.string(),
|
||||||
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
|
userId: v.string(),
|
||||||
|
benefitId: v.string(),
|
||||||
|
properties: v.record(v.string(), v.any()),
|
||||||
|
isGranted: v.boolean(),
|
||||||
|
isRevoked: v.boolean(),
|
||||||
|
subscriptionId: v.union(v.string(), v.null()),
|
||||||
|
orderId: v.union(v.string(), v.null()),
|
||||||
|
grantedAt: v.union(v.string(), v.null()),
|
||||||
|
revokedAt: v.union(v.string(), v.null()),
|
||||||
})
|
})
|
||||||
.index("key", ["key"])
|
.index("id", ["id"])
|
||||||
.index("polarProductId", ["polarProductId"]),
|
.index("userId", ["userId"]),
|
||||||
|
orders: defineTable({
|
||||||
|
id: v.string(),
|
||||||
|
createdAt: v.string(),
|
||||||
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
|
userId: v.union(v.string(), v.null()),
|
||||||
|
productId: v.union(v.string(), v.null()),
|
||||||
|
productPriceId: v.string(),
|
||||||
|
subscriptionId: v.union(v.string(), v.null()),
|
||||||
|
checkoutId: v.union(v.string(), v.null()),
|
||||||
|
metadata: v.record(v.string(), v.any()),
|
||||||
|
amount: v.number(),
|
||||||
|
taxAmount: v.number(),
|
||||||
|
currency: v.string(),
|
||||||
|
billingReason: v.string(),
|
||||||
|
})
|
||||||
|
.index("id", ["id"])
|
||||||
|
.index("userId", ["userId"]),
|
||||||
|
products: defineTable({
|
||||||
|
id: v.string(),
|
||||||
|
createdAt: v.string(),
|
||||||
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
|
name: v.string(),
|
||||||
|
description: v.union(v.string(), v.null()),
|
||||||
|
isRecurring: v.boolean(),
|
||||||
|
isArchived: v.boolean(),
|
||||||
|
organizationId: v.string(),
|
||||||
|
prices: v.array(
|
||||||
|
v.object({
|
||||||
|
id: v.string(),
|
||||||
|
createdAt: v.string(),
|
||||||
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
|
amountType: v.optional(v.string()),
|
||||||
|
isArchived: v.boolean(),
|
||||||
|
productId: v.string(),
|
||||||
|
priceCurrency: v.optional(v.string()),
|
||||||
|
priceAmount: v.optional(v.number()),
|
||||||
|
type: v.optional(v.string()),
|
||||||
|
recurringInterval: v.optional(v.string()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
medias: v.array(
|
||||||
|
v.object({
|
||||||
|
id: v.string(),
|
||||||
|
organizationId: v.string(),
|
||||||
|
name: v.string(),
|
||||||
|
path: v.string(),
|
||||||
|
mimeType: v.string(),
|
||||||
|
size: v.number(),
|
||||||
|
storageVersion: v.union(v.string(), v.null()),
|
||||||
|
checksumEtag: v.union(v.string(), v.null()),
|
||||||
|
checksumSha256Base64: v.union(v.string(), v.null()),
|
||||||
|
checksumSha256Hex: v.union(v.string(), v.null()),
|
||||||
|
createdAt: v.string(),
|
||||||
|
lastModifiedAt: v.union(v.string(), v.null()),
|
||||||
|
version: v.union(v.string(), v.null()),
|
||||||
|
service: v.optional(v.string()),
|
||||||
|
isUploaded: v.boolean(),
|
||||||
|
sizeReadable: v.string(),
|
||||||
|
publicUrl: v.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.index("id", ["id"])
|
||||||
|
.index("isArchived", ["isArchived"]),
|
||||||
subscriptions: defineTable({
|
subscriptions: defineTable({
|
||||||
planId: v.id("plans"),
|
id: v.string(),
|
||||||
polarId: v.string(),
|
createdAt: v.string(),
|
||||||
polarPriceId: v.string(),
|
modifiedAt: v.union(v.string(), v.null()),
|
||||||
currency: currencyValidator,
|
amount: v.union(v.number(), v.null()),
|
||||||
interval: intervalValidator,
|
currency: v.union(v.string(), v.null()),
|
||||||
|
recurringInterval: v.string(),
|
||||||
status: v.string(),
|
status: v.string(),
|
||||||
currentPeriodStart: v.optional(v.number()),
|
currentPeriodStart: v.string(),
|
||||||
currentPeriodEnd: v.optional(v.number()),
|
currentPeriodEnd: v.union(v.string(), v.null()),
|
||||||
cancelAtPeriodEnd: v.optional(v.boolean()),
|
cancelAtPeriodEnd: v.boolean(),
|
||||||
localUserId: v.id("users"),
|
startedAt: v.union(v.string(), v.null()),
|
||||||
|
endedAt: v.union(v.string(), v.null()),
|
||||||
|
userId: v.string(),
|
||||||
|
productId: v.string(),
|
||||||
|
priceId: v.string(),
|
||||||
|
checkoutId: v.union(v.string(), v.null()),
|
||||||
|
metadata: v.record(v.string(), v.any()),
|
||||||
})
|
})
|
||||||
.index("localUserId", ["localUserId"])
|
.index("id", ["id"])
|
||||||
.index("polarId", ["polarId"]),
|
.index("userId", ["userId"])
|
||||||
});
|
.index("userId_status", ["userId", "status"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
schemaValidation: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GenericQueryCtx } from "convex/server";
|
import { GenericQueryCtx, WithoutSystemFields } from "convex/server";
|
||||||
import { Expand, FunctionReference } from "convex/server";
|
import { Expand, FunctionReference } from "convex/server";
|
||||||
|
|
||||||
import { GenericMutationCtx } from "convex/server";
|
import { GenericMutationCtx } from "convex/server";
|
||||||
@@ -6,6 +6,14 @@ import { GenericDataModel } from "convex/server";
|
|||||||
import { GenericActionCtx } from "convex/server";
|
import { GenericActionCtx } from "convex/server";
|
||||||
import { GenericId } from "convex/values";
|
import { GenericId } from "convex/values";
|
||||||
import { Mounts } from "./_generated/api";
|
import { Mounts } from "./_generated/api";
|
||||||
|
import {
|
||||||
|
Benefit,
|
||||||
|
BenefitGrant,
|
||||||
|
Order,
|
||||||
|
Product,
|
||||||
|
Subscription,
|
||||||
|
} from "@polar-sh/sdk/models/components";
|
||||||
|
import { Doc } from "./_generated/dataModel";
|
||||||
|
|
||||||
export type RunQueryCtx = {
|
export type RunQueryCtx = {
|
||||||
runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
|
runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
|
||||||
@@ -45,3 +53,137 @@ export type UseApi<API> = Expand<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ComponentApi = UseApi<Mounts>;
|
export type ComponentApi = UseApi<Mounts>;
|
||||||
|
|
||||||
|
export const convertToDatabaseOrder = (
|
||||||
|
order: Order
|
||||||
|
): WithoutSystemFields<Doc<"orders">> => {
|
||||||
|
return {
|
||||||
|
id: order.id,
|
||||||
|
userId: order.userId,
|
||||||
|
productId: order.productId,
|
||||||
|
productPriceId: order.productPriceId,
|
||||||
|
subscriptionId: order.subscriptionId,
|
||||||
|
checkoutId: order.checkoutId,
|
||||||
|
createdAt: order.createdAt.toISOString(),
|
||||||
|
modifiedAt: order.modifiedAt?.toISOString() ?? null,
|
||||||
|
metadata: order.metadata,
|
||||||
|
amount: order.amount,
|
||||||
|
taxAmount: order.taxAmount,
|
||||||
|
currency: order.currency,
|
||||||
|
billingReason: order.billingReason,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToDatabaseSubscription = (
|
||||||
|
subscription: Subscription
|
||||||
|
): WithoutSystemFields<Doc<"subscriptions">> => {
|
||||||
|
return {
|
||||||
|
id: subscription.id,
|
||||||
|
createdAt: subscription.createdAt.toISOString(),
|
||||||
|
modifiedAt: subscription.modifiedAt?.toISOString() ?? null,
|
||||||
|
userId: subscription.userId,
|
||||||
|
productId: subscription.productId,
|
||||||
|
priceId: subscription.priceId,
|
||||||
|
checkoutId: subscription.checkoutId,
|
||||||
|
amount: subscription.amount,
|
||||||
|
currency: subscription.currency,
|
||||||
|
recurringInterval: subscription.recurringInterval,
|
||||||
|
status: subscription.status,
|
||||||
|
currentPeriodStart: subscription.currentPeriodStart.toISOString(),
|
||||||
|
currentPeriodEnd: subscription.currentPeriodEnd?.toISOString() ?? null,
|
||||||
|
cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
|
||||||
|
startedAt: subscription.startedAt?.toISOString() ?? null,
|
||||||
|
endedAt: subscription.endedAt?.toISOString() ?? null,
|
||||||
|
metadata: subscription.metadata,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToDatabaseProduct = (
|
||||||
|
product: Product
|
||||||
|
): WithoutSystemFields<Doc<"products">> => {
|
||||||
|
return {
|
||||||
|
id: product.id,
|
||||||
|
organizationId: product.organizationId,
|
||||||
|
name: product.name,
|
||||||
|
description: product.description,
|
||||||
|
isRecurring: product.isRecurring,
|
||||||
|
isArchived: product.isArchived,
|
||||||
|
createdAt: product.createdAt.toISOString(),
|
||||||
|
modifiedAt: product.modifiedAt?.toISOString() ?? null,
|
||||||
|
prices: product.prices.map((price) => ({
|
||||||
|
id: price.id,
|
||||||
|
productId: price.productId,
|
||||||
|
amountType: price.amountType,
|
||||||
|
isArchived: price.isArchived,
|
||||||
|
createdAt: price.createdAt.toISOString(),
|
||||||
|
modifiedAt: price.modifiedAt?.toISOString() ?? null,
|
||||||
|
recurringInterval:
|
||||||
|
price.type === "recurring" ? price.recurringInterval : undefined,
|
||||||
|
priceAmount: price.amountType === "fixed" ? price.priceAmount : undefined,
|
||||||
|
priceCurrency:
|
||||||
|
price.amountType === "fixed" || price.amountType === "custom"
|
||||||
|
? price.priceCurrency
|
||||||
|
: undefined,
|
||||||
|
minimumAmount:
|
||||||
|
price.amountType === "custom" ? price.minimumAmount : undefined,
|
||||||
|
maximumAmount:
|
||||||
|
price.amountType === "custom" ? price.maximumAmount : undefined,
|
||||||
|
presetAmount:
|
||||||
|
price.amountType === "custom" ? price.presetAmount : undefined,
|
||||||
|
type: price.type,
|
||||||
|
})),
|
||||||
|
medias: product.medias.map((media) => ({
|
||||||
|
id: media.id,
|
||||||
|
organizationId: media.organizationId,
|
||||||
|
name: media.name,
|
||||||
|
path: media.path,
|
||||||
|
mimeType: media.mimeType,
|
||||||
|
size: media.size,
|
||||||
|
storageVersion: media.storageVersion,
|
||||||
|
checksumEtag: media.checksumEtag,
|
||||||
|
checksumSha256Base64: media.checksumSha256Base64,
|
||||||
|
checksumSha256Hex: media.checksumSha256Hex,
|
||||||
|
createdAt: media.createdAt.toISOString(),
|
||||||
|
lastModifiedAt: media.lastModifiedAt?.toISOString() ?? null,
|
||||||
|
version: media.version,
|
||||||
|
isUploaded: media.isUploaded,
|
||||||
|
sizeReadable: media.sizeReadable,
|
||||||
|
publicUrl: media.publicUrl,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToDatabaseBenefit = (
|
||||||
|
benefit: Benefit
|
||||||
|
): WithoutSystemFields<Doc<"benefits">> => {
|
||||||
|
return {
|
||||||
|
id: benefit.id,
|
||||||
|
organizationId: benefit.organizationId,
|
||||||
|
description: benefit.description,
|
||||||
|
selectable: benefit.selectable,
|
||||||
|
deletable: benefit.deletable,
|
||||||
|
properties: benefit.properties,
|
||||||
|
createdAt: benefit.createdAt.toISOString(),
|
||||||
|
modifiedAt: benefit.modifiedAt?.toISOString() ?? null,
|
||||||
|
type: benefit.type,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertToDatabaseBenefitGrant = (
|
||||||
|
benefitGrant: BenefitGrant
|
||||||
|
): WithoutSystemFields<Doc<"benefitGrants">> => {
|
||||||
|
return {
|
||||||
|
id: benefitGrant.id,
|
||||||
|
userId: benefitGrant.userId,
|
||||||
|
benefitId: benefitGrant.benefitId,
|
||||||
|
properties: benefitGrant.properties,
|
||||||
|
isGranted: benefitGrant.isGranted,
|
||||||
|
isRevoked: benefitGrant.isRevoked,
|
||||||
|
subscriptionId: benefitGrant.subscriptionId,
|
||||||
|
orderId: benefitGrant.orderId,
|
||||||
|
createdAt: benefitGrant.createdAt.toISOString(),
|
||||||
|
modifiedAt: benefitGrant.modifiedAt?.toISOString() ?? null,
|
||||||
|
grantedAt: benefitGrant.grantedAt?.toISOString() ?? null,
|
||||||
|
revokedAt: benefitGrant.revokedAt?.toISOString() ?? null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import { type GenericActionCtx, type GenericDataModel } from "convex/server";
|
|
||||||
|
|
||||||
import {
|
|
||||||
type WebhookSubscriptionCreatedPayload,
|
|
||||||
type WebhookSubscriptionCreatedPayload$Outbound,
|
|
||||||
WebhookSubscriptionCreatedPayload$inboundSchema as WebhookSubscriptionCreatedPayloadSchema,
|
|
||||||
} from "@polar-sh/sdk/models/components/webhooksubscriptioncreatedpayload";
|
|
||||||
import {
|
|
||||||
type WebhookSubscriptionUpdatedPayload,
|
|
||||||
type WebhookSubscriptionUpdatedPayload$Outbound,
|
|
||||||
WebhookSubscriptionUpdatedPayload$inboundSchema as WebhookSubscriptionUpdatedPayloadSchema,
|
|
||||||
} from "@polar-sh/sdk/models/components/webhooksubscriptionupdatedpayload";
|
|
||||||
import { Webhook } from "standardwebhooks";
|
|
||||||
import type { Doc } from "../component/_generated/dataModel";
|
|
||||||
import {
|
|
||||||
sendSubscriptionErrorEmail,
|
|
||||||
sendSubscriptionSuccessEmail,
|
|
||||||
} from "../component/email/templates/subscriptionEmail";
|
|
||||||
import { ComponentApi } from "./util";
|
|
||||||
|
|
||||||
const handleUpdateSubscription = async (
|
|
||||||
component: ComponentApi,
|
|
||||||
ctx: GenericActionCtx<GenericDataModel>,
|
|
||||||
user: Doc<"users">,
|
|
||||||
subscription:
|
|
||||||
| WebhookSubscriptionCreatedPayload
|
|
||||||
| WebhookSubscriptionUpdatedPayload
|
|
||||||
) => {
|
|
||||||
const subscriptionItem = subscription.data;
|
|
||||||
await ctx.runMutation(component.lib.replaceSubscription, {
|
|
||||||
localUserId: user._id,
|
|
||||||
subscriptionPolarId: subscription.data.id,
|
|
||||||
input: {
|
|
||||||
productId: subscriptionItem.productId,
|
|
||||||
priceId: subscriptionItem.priceId,
|
|
||||||
interval: subscriptionItem.recurringInterval,
|
|
||||||
status: subscriptionItem.status,
|
|
||||||
currency: "usd",
|
|
||||||
currentPeriodStart: subscriptionItem.currentPeriodStart.getTime(),
|
|
||||||
currentPeriodEnd: subscriptionItem.currentPeriodEnd?.getTime(),
|
|
||||||
cancelAtPeriodEnd: subscriptionItem.cancelAtPeriodEnd,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubscriptionChange = async (
|
|
||||||
component: ComponentApi,
|
|
||||||
ctx: GenericActionCtx<GenericDataModel>,
|
|
||||||
event: WebhookSubscriptionCreatedPayload | WebhookSubscriptionUpdatedPayload
|
|
||||||
) => {
|
|
||||||
const userId = event.data.metadata.userId;
|
|
||||||
const email = event.data.user.email;
|
|
||||||
const user = await ctx.runQuery(component.lib.getUserByLocalId, {
|
|
||||||
localUserId: userId,
|
|
||||||
});
|
|
||||||
if (!user) {
|
|
||||||
throw new Error("User not found");
|
|
||||||
}
|
|
||||||
await handleUpdateSubscription(component, ctx, user, event);
|
|
||||||
|
|
||||||
const freePlan = await ctx.runQuery(component.lib.getPlanByKey, {
|
|
||||||
key: "free",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only send email for paid plans
|
|
||||||
if (event.data.productId !== freePlan?.polarProductId) {
|
|
||||||
await sendSubscriptionSuccessEmail({
|
|
||||||
email,
|
|
||||||
subscriptionId: event.data.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePolarSubscriptionUpdatedError = async (
|
|
||||||
component: ComponentApi,
|
|
||||||
ctx: GenericActionCtx<GenericDataModel>,
|
|
||||||
event: WebhookSubscriptionCreatedPayload | WebhookSubscriptionUpdatedPayload
|
|
||||||
) => {
|
|
||||||
const userId = event.data.metadata.userId;
|
|
||||||
const email = event.data.user.email;
|
|
||||||
const subscription = event.data;
|
|
||||||
|
|
||||||
const user = await ctx.runQuery(component.lib.getUserByLocalId, {
|
|
||||||
localUserId: userId,
|
|
||||||
});
|
|
||||||
if (!user) throw new Error("User not found");
|
|
||||||
|
|
||||||
const freePlan = await ctx.runQuery(component.lib.getPlanByKey, {
|
|
||||||
key: "free",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Only send email for paid plans
|
|
||||||
if (event.data.productId !== freePlan?.polarProductId) {
|
|
||||||
await sendSubscriptionErrorEmail({
|
|
||||||
email,
|
|
||||||
subscriptionId: subscription.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Response(null);
|
|
||||||
};
|
|
||||||
export const handleWebhook = async (
|
|
||||||
component: ComponentApi,
|
|
||||||
ctx: GenericActionCtx<GenericDataModel>,
|
|
||||||
request: Request
|
|
||||||
) => {
|
|
||||||
if (!request.body) {
|
|
||||||
return new Response(null, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const wh = new Webhook(btoa(process.env.POLAR_WEBHOOK_SECRET!));
|
|
||||||
const body = await request.text();
|
|
||||||
const event = wh.verify(
|
|
||||||
body,
|
|
||||||
Object.fromEntries(request.headers.entries())
|
|
||||||
) as
|
|
||||||
| WebhookSubscriptionCreatedPayload$Outbound
|
|
||||||
| WebhookSubscriptionUpdatedPayload$Outbound
|
|
||||||
| { type: string };
|
|
||||||
|
|
||||||
console.log("event", event);
|
|
||||||
try {
|
|
||||||
switch (event.type) {
|
|
||||||
/**
|
|
||||||
* Occurs when a subscription has been created.
|
|
||||||
*/
|
|
||||||
case "subscription.created": {
|
|
||||||
return handleSubscriptionChange(
|
|
||||||
component,
|
|
||||||
ctx,
|
|
||||||
WebhookSubscriptionCreatedPayloadSchema.parse(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Occurs when a subscription has been updated.
|
|
||||||
* E.g. when a user upgrades or downgrades their plan.
|
|
||||||
*/
|
|
||||||
case "subscription.updated": {
|
|
||||||
return handleSubscriptionChange(
|
|
||||||
component,
|
|
||||||
ctx,
|
|
||||||
WebhookSubscriptionUpdatedPayloadSchema.parse(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
switch (event.type) {
|
|
||||||
case "subscription.created": {
|
|
||||||
return handlePolarSubscriptionUpdatedError(
|
|
||||||
component,
|
|
||||||
ctx,
|
|
||||||
WebhookSubscriptionCreatedPayloadSchema.parse(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
case "subscription.updated": {
|
|
||||||
return handlePolarSubscriptionUpdatedError(
|
|
||||||
component,
|
|
||||||
ctx,
|
|
||||||
WebhookSubscriptionUpdatedPayloadSchema.parse(event)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Response("OK", { status: 200 });
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user