mirror of
https://github.com/LukeHagar/polar.git
synced 2025-12-10 04:21:09 +00:00
wip
This commit is contained in:
17
example/components.json
Normal file
17
example/components.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
||||||
224
example/convex/_generated/api.d.ts
vendored
224
example/convex/_generated/api.d.ts
vendored
@@ -42,66 +42,80 @@ export declare const internal: FilterApi<
|
|||||||
export declare const components: {
|
export declare const components: {
|
||||||
polar: {
|
polar: {
|
||||||
lib: {
|
lib: {
|
||||||
getBenefit: FunctionReference<
|
createProduct: FunctionReference<
|
||||||
"query",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ id: string },
|
|
||||||
{
|
{
|
||||||
_creationTime: number;
|
product: {
|
||||||
_id: string;
|
createdAt: string;
|
||||||
createdAt: string;
|
description: string | null;
|
||||||
deletable: boolean;
|
id: string;
|
||||||
description: string;
|
isArchived: boolean;
|
||||||
id: string;
|
isRecurring: boolean;
|
||||||
modifiedAt: string | null;
|
medias: Array<{
|
||||||
organizationId: string;
|
checksumEtag: string | null;
|
||||||
properties: Record<string, any>;
|
checksumSha256Base64: string | null;
|
||||||
selectable: boolean;
|
checksumSha256Hex: string | null;
|
||||||
type?: string;
|
createdAt: string;
|
||||||
} | null
|
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
|
||||||
>;
|
>;
|
||||||
getBenefitGrant: FunctionReference<
|
createSubscription: FunctionReference<
|
||||||
"query",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ id: string },
|
|
||||||
{
|
{
|
||||||
_creationTime: number;
|
callback?: string;
|
||||||
_id: string;
|
subscription: {
|
||||||
benefitId: string;
|
amount: number | null;
|
||||||
createdAt: string;
|
cancelAtPeriodEnd: boolean;
|
||||||
grantedAt: string | null;
|
checkoutId: string | null;
|
||||||
id: string;
|
createdAt: string;
|
||||||
isGranted: boolean;
|
currency: string | null;
|
||||||
isRevoked: boolean;
|
currentPeriodEnd: string | null;
|
||||||
modifiedAt: string | null;
|
currentPeriodStart: string;
|
||||||
orderId: string | null;
|
endedAt: string | null;
|
||||||
properties: Record<string, any>;
|
id: string;
|
||||||
revokedAt: string | null;
|
metadata: Record<string, any>;
|
||||||
subscriptionId: string | null;
|
modifiedAt: string | null;
|
||||||
userId: string;
|
priceId: string;
|
||||||
} | null
|
productId: string;
|
||||||
>;
|
recurringInterval: string;
|
||||||
getOrder: FunctionReference<
|
startedAt: string | null;
|
||||||
"query",
|
status: string;
|
||||||
"internal",
|
userId: string;
|
||||||
{ id: string },
|
};
|
||||||
{
|
},
|
||||||
_creationTime: number;
|
any
|
||||||
_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<
|
getProduct: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
@@ -177,24 +191,6 @@ export declare const components: {
|
|||||||
userId: string;
|
userId: string;
|
||||||
} | null
|
} | null
|
||||||
>;
|
>;
|
||||||
listBenefits: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"internal",
|
|
||||||
{},
|
|
||||||
Array<{
|
|
||||||
_creationTime: number;
|
|
||||||
_id: string;
|
|
||||||
createdAt: string;
|
|
||||||
deletable: boolean;
|
|
||||||
description: string;
|
|
||||||
id: string;
|
|
||||||
modifiedAt: string | null;
|
|
||||||
organizationId: string;
|
|
||||||
properties: Record<string, any>;
|
|
||||||
selectable: boolean;
|
|
||||||
type?: string;
|
|
||||||
}>
|
|
||||||
>;
|
|
||||||
listProducts: FunctionReference<
|
listProducts: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
@@ -243,27 +239,6 @@ export declare const components: {
|
|||||||
}>;
|
}>;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
listUserBenefitGrants: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"internal",
|
|
||||||
{ userId: string },
|
|
||||||
Array<{
|
|
||||||
_creationTime: number;
|
|
||||||
_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;
|
|
||||||
}>
|
|
||||||
>;
|
|
||||||
listUserSubscriptions: FunctionReference<
|
listUserSubscriptions: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"internal",
|
"internal",
|
||||||
@@ -333,67 +308,6 @@ export declare const components: {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
updateBenefit: 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
|
|
||||||
>;
|
|
||||||
updateBenefitGrant: 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
|
|
||||||
>;
|
|
||||||
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<
|
updateProduct: FunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
|
|||||||
@@ -1,58 +1,6 @@
|
|||||||
import { Polar } from "@convex-dev/polar";
|
import { Polar } from "@convex-dev/polar";
|
||||||
import { v } from "convex/values";
|
import { v } from "convex/values";
|
||||||
import { WebhookSubscriptionCreatedPayload$inboundSchema } from "@polar-sh/sdk/models/components";
|
import { query } from "./_generated/server";
|
||||||
import { query, internalMutation } from "./_generated/server";
|
|
||||||
import { components } from "./_generated/api";
|
import { components } from "./_generated/api";
|
||||||
import { Id } from "./_generated/dataModel";
|
|
||||||
|
|
||||||
const polar = new Polar(components.polar);
|
export const polar = new Polar(components.polar);
|
||||||
|
|
||||||
export const listProducts = query({
|
|
||||||
args: {},
|
|
||||||
handler: async (ctx) => {
|
|
||||||
return ctx.runQuery(polar.component.lib.listProducts, {
|
|
||||||
includeArchived: false,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listUserSubscriptions = query({
|
|
||||||
args: {
|
|
||||||
userId: v.string(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
return ctx.runQuery(polar.component.lib.listUserSubscriptions, {
|
|
||||||
userId: args.userId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function is called when a Polar webhook is received.
|
|
||||||
*
|
|
||||||
* The payload is provided as received from Polar, and the webhook signature is
|
|
||||||
* already verified before this function is called.
|
|
||||||
*/
|
|
||||||
export const polarEventCallback = internalMutation({
|
|
||||||
args: {
|
|
||||||
payload: v.any(),
|
|
||||||
},
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
switch (args.payload.type) {
|
|
||||||
// When creating a subscription, pass the user's id from your system into
|
|
||||||
// the metadata field. The same metadata will be passed back in the
|
|
||||||
// webhook, allowing you to add the user's Polar ID to the record in
|
|
||||||
// your database.
|
|
||||||
case "subscription.created": {
|
|
||||||
const payload = WebhookSubscriptionCreatedPayload$inboundSchema.parse(
|
|
||||||
args.payload
|
|
||||||
);
|
|
||||||
const userId = payload.data.metadata.userId;
|
|
||||||
await ctx.db.patch(userId as Id<"users">, {
|
|
||||||
polarId: payload.data.userId,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Polar } from "@convex-dev/polar";
|
import { Polar } from "@convex-dev/polar";
|
||||||
import { httpRouter } from "convex/server";
|
import { httpRouter } from "convex/server";
|
||||||
import { components, internal } from "./_generated/api";
|
import { components } from "./_generated/api";
|
||||||
|
|
||||||
const http = httpRouter();
|
const http = httpRouter();
|
||||||
|
|
||||||
const polar = new Polar(components.polar);
|
const polar = new Polar(components.polar);
|
||||||
|
|
||||||
polar.registerRoutes(http, {
|
polar.registerRoutes(http, {
|
||||||
|
// Optional custom path, default is "/events/polar"
|
||||||
path: "/events/polar",
|
path: "/events/polar",
|
||||||
eventCallback: internal.example.polarEventCallback,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|||||||
@@ -4,16 +4,28 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "convex dev --live-component-sources --typecheck-components",
|
"dev": "convex dev --live-component-sources --typecheck-components --tail-logs",
|
||||||
"dev:frontend": "vite",
|
"dev:frontend": "vite",
|
||||||
"logs": "convex logs",
|
"logs": "convex logs",
|
||||||
"lint": "tsc -p convex && eslint convex"
|
"lint": "tsc -p convex && eslint convex"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/polar": "file:..",
|
"@convex-dev/polar": "file:..",
|
||||||
|
"@radix-ui/react-separator": "^1.1.1",
|
||||||
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
|
"@tanstack/react-table": "^8.20.6",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
"convex": "file:../node_modules/convex",
|
"convex": "file:../node_modules/convex",
|
||||||
|
"convex-helpers": "^0.1.67",
|
||||||
|
"lucide-react": "^0.471.0",
|
||||||
|
"next-themes": "^0.4.4",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"use-debounce": "^10.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
|
|||||||
6
example/postcss.config.js
Normal file
6
example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@@ -1,23 +1,95 @@
|
|||||||
import "./App.css";
|
import { useState } from "react";
|
||||||
import { useMutation, useQuery } from "convex/react";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "../convex/_generated/api";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { UpgradeCTA } from "./UpgradeCta";
|
||||||
|
import { AlertCircle } from "lucide-react";
|
||||||
|
|
||||||
function App() {
|
interface Todo {
|
||||||
const count = useQuery(api.example.getCount);
|
id: number;
|
||||||
const addOne = useMutation(api.example.addOne);
|
text: string;
|
||||||
|
completed: boolean;
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Convex Polar Component Example</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => addOne()}>count is {count}</button>
|
|
||||||
<p>
|
|
||||||
See <code>example/convex/example.ts</code> for all the ways to use
|
|
||||||
this component
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default function TodoList() {
|
||||||
|
const [todos, setTodos] = useState<Todo[]>([]);
|
||||||
|
const [newTodo, setNewTodo] = useState("");
|
||||||
|
const [isPremium, setIsPremium] = useState(false); // This would typically be set based on user's subscription status
|
||||||
|
|
||||||
|
const MAX_FREE_TODOS = 5;
|
||||||
|
|
||||||
|
const addTodo = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (newTodo.trim()) {
|
||||||
|
if (!isPremium && todos.length >= MAX_FREE_TODOS) {
|
||||||
|
alert(
|
||||||
|
"You've reached the maximum number of todos for the free plan. Please upgrade to add more!"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTodos([
|
||||||
|
...todos,
|
||||||
|
{ id: Date.now(), text: newTodo.trim(), completed: false },
|
||||||
|
]);
|
||||||
|
setNewTodo("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTodo = (id: number) => {
|
||||||
|
setTodos(
|
||||||
|
todos.map((todo) =>
|
||||||
|
todo.id === id ? { ...todo, completed: !todo.completed } : todo
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteTodo = (id: number) => {
|
||||||
|
setTodos(todos.filter((todo) => todo.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto p-6">
|
||||||
|
<h1 className="text-3xl font-light mb-6 text-gray-800">Todo List</h1>
|
||||||
|
<form onSubmit={addTodo} className="mb-6">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={newTodo}
|
||||||
|
onChange={(e) => setNewTodo(e.target.value)}
|
||||||
|
placeholder="Add a new task"
|
||||||
|
className="w-full text-lg py-2 border-b border-gray-300 focus:border-gray-500 transition-colors duration-300"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
{!isPremium && todos.length >= MAX_FREE_TODOS && (
|
||||||
|
<div className="flex items-center text-yellow-600 mb-4">
|
||||||
|
<AlertCircle className="mr-2" />
|
||||||
|
<span>
|
||||||
|
You've reached the limit for free todos. Upgrade to add more!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{todos.map((todo) => (
|
||||||
|
<li
|
||||||
|
key={todo.id}
|
||||||
|
className="flex items-center justify-between py-2 border-b border-gray-200"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => toggleTodo(todo.id)}
|
||||||
|
className={`text-lg flex-grow text-left ${todo.completed ? "line-through text-gray-400" : "text-gray-800"}`}
|
||||||
|
>
|
||||||
|
{todo.text}
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => deleteTodo(todo.id)}
|
||||||
|
className="text-gray-400 hover:text-red-500"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{!isPremium && <UpgradeCTA />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
17
example/src/UpgradeCta.tsx
Normal file
17
example/src/UpgradeCta.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
|
||||||
|
export function UpgradeCTA() {
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-r from-purple-500 to-indigo-500 text-white p-4 rounded-lg shadow-md mt-8">
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Upgrade to Premium</h2>
|
||||||
|
<p className="mb-4">Get unlimited todos, priority support, and more!</p>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-white text-purple-700 hover:bg-gray-100"
|
||||||
|
>
|
||||||
|
Upgrade Now <ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
57
example/src/components/ui/button.tsx
Normal file
57
example/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button";
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonVariants };
|
||||||
24
example/src/components/ui/input.tsx
Normal file
24
example/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
@@ -1,68 +1,78 @@
|
|||||||
:root {
|
@tailwind base;
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
@tailwind components;
|
||||||
line-height: 1.5;
|
@tailwind utilities;
|
||||||
font-weight: 400;
|
|
||||||
|
|
||||||
color-scheme: light dark;
|
/* To change the theme colors, change the values below
|
||||||
color: rgba(255, 255, 255, 0.87);
|
or use the "Copy code" button at https://ui.shadcn.com/themes */
|
||||||
background-color: #242424;
|
@layer base {
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
min-width: 320px;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 3.2em;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
padding: 0.6em 1.2em;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: inherit;
|
|
||||||
background-color: #1a1a1a;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color 0.25s;
|
|
||||||
}
|
|
||||||
button:hover {
|
|
||||||
border-color: #646cff;
|
|
||||||
}
|
|
||||||
button:focus,
|
|
||||||
button:focus-visible {
|
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
|
||||||
:root {
|
:root {
|
||||||
color: #213547;
|
--background: 0 0% 100%;
|
||||||
background-color: #ffffff;
|
--foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--primary: 24 9.8% 10%;
|
||||||
|
--primary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--secondary: 60 4.8% 95.9%;
|
||||||
|
--secondary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--muted: 60 4.8% 95.9%;
|
||||||
|
--muted-foreground: 25 5.3% 44.7%;
|
||||||
|
|
||||||
|
--accent: 60 4.8% 95.9%;
|
||||||
|
--accent-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 20 5.9% 90%;
|
||||||
|
--input: 20 5.9% 90%;
|
||||||
|
--ring: 20 14.3% 4.1%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
a:hover {
|
|
||||||
color: #747bff;
|
.dark {
|
||||||
}
|
--background: 20 14.3% 4.1%;
|
||||||
button {
|
--foreground: 60 9.1% 97.8%;
|
||||||
background-color: #f9f9f9;
|
|
||||||
|
--card: 20 14.3% 4.1%;
|
||||||
|
--card-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--popover: 20 14.3% 4.1%;
|
||||||
|
--popover-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--primary: 60 9.1% 97.8%;
|
||||||
|
--primary-foreground: 24 9.8% 10%;
|
||||||
|
|
||||||
|
--secondary: 12 6.5% 15.1%;
|
||||||
|
--secondary-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--muted: 12 6.5% 15.1%;
|
||||||
|
--muted-foreground: 24 5.4% 63.9%;
|
||||||
|
|
||||||
|
--accent: 12 6.5% 15.1%;
|
||||||
|
--accent-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 60 9.1% 97.8%;
|
||||||
|
|
||||||
|
--border: 12 6.5% 15.1%;
|
||||||
|
--input: 12 6.5% 15.1%;
|
||||||
|
--ring: 24 5.7% 82.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
example/src/lib/utils.ts
Normal file
6
example/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import { StrictMode } from "react";
|
import React from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||||
import App from "./App.tsx";
|
import App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
const address = import.meta.env.VITE_CONVEX_URL;
|
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
|
||||||
|
|
||||||
const convex = new ConvexReactClient(address);
|
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||||
|
<React.StrictMode>
|
||||||
createRoot(document.getElementById("root")!).render(
|
<ThemeProvider attribute="class">
|
||||||
<StrictMode>
|
<ConvexProvider client={convex}>
|
||||||
<ConvexProvider client={convex}>
|
<App />
|
||||||
<App />
|
</ConvexProvider>
|
||||||
</ConvexProvider>
|
</ThemeProvider>
|
||||||
</StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
77
example/tailwind.config.js
Normal file
77
example/tailwind.config.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: "selector",
|
||||||
|
content: [
|
||||||
|
"./pages/**/*.{ts,tsx}",
|
||||||
|
"./components/**/*.{ts,tsx}",
|
||||||
|
"./app/**/*.{ts,tsx}",
|
||||||
|
"./src/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
safelist: ["dark"],
|
||||||
|
prefix: "",
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
sm: "1000px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Import paths */
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
/* This should only be used in this example. Real apps should not attempt
|
/* This should only be used in this example. Real apps should not attempt
|
||||||
* to compile TypeScript because differences between tsconfig.json files can
|
* to compile TypeScript because differences between tsconfig.json files can
|
||||||
* cause the code to be compiled differently.
|
* cause the code to be compiled differently.
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import path from "path";
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
conditions: ["@convex-dev/component-source"],
|
conditions: ["@convex-dev/component-source"],
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"convex": "^1.17.0",
|
"@polar-sh/sdk": "^0.20.2",
|
||||||
"@polar-sh/sdk": "~0.16.0"
|
"convex": "^1.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.9.1",
|
"@eslint/js": "^9.9.1",
|
||||||
@@ -86,6 +86,7 @@
|
|||||||
"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": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"convex-helpers": "^0.1.63",
|
"convex-helpers": "^0.1.63",
|
||||||
"standardwebhooks": "^1.0.0"
|
"standardwebhooks": "^1.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,55 @@
|
|||||||
|
import "./polyfill";
|
||||||
import {
|
import {
|
||||||
type WebhookBenefitCreatedPayload$Outbound,
|
FunctionReference,
|
||||||
WebhookBenefitCreatedPayload$inboundSchema,
|
|
||||||
type WebhookBenefitGrantCreatedPayload$Outbound,
|
|
||||||
WebhookBenefitGrantCreatedPayload$inboundSchema,
|
|
||||||
type WebhookBenefitGrantUpdatedPayload$Outbound,
|
|
||||||
WebhookBenefitGrantUpdatedPayload$inboundSchema,
|
|
||||||
type WebhookBenefitUpdatedPayload$Outbound,
|
|
||||||
WebhookBenefitUpdatedPayload$inboundSchema,
|
|
||||||
type WebhookOrderCreatedPayload$Outbound,
|
|
||||||
WebhookOrderCreatedPayload$inboundSchema,
|
|
||||||
type WebhookProductCreatedPayload$Outbound,
|
|
||||||
WebhookProductCreatedPayload$inboundSchema,
|
|
||||||
type WebhookProductUpdatedPayload$Outbound,
|
|
||||||
WebhookProductUpdatedPayload$inboundSchema,
|
|
||||||
type WebhookSubscriptionCreatedPayload$Outbound,
|
|
||||||
WebhookSubscriptionCreatedPayload$inboundSchema,
|
|
||||||
type WebhookSubscriptionUpdatedPayload$Outbound,
|
|
||||||
WebhookSubscriptionUpdatedPayload$inboundSchema,
|
|
||||||
} from "@polar-sh/sdk/models/components";
|
|
||||||
import {
|
|
||||||
type FunctionReference,
|
|
||||||
type HttpRouter,
|
type HttpRouter,
|
||||||
|
WithoutSystemFields,
|
||||||
createFunctionHandle,
|
createFunctionHandle,
|
||||||
httpActionGeneric,
|
httpActionGeneric,
|
||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
import { Webhook } from "standardwebhooks";
|
|
||||||
import {
|
import {
|
||||||
type ComponentApi,
|
type ComponentApi,
|
||||||
convertToDatabaseBenefit,
|
|
||||||
convertToDatabaseBenefitGrant,
|
|
||||||
convertToDatabaseOrder,
|
|
||||||
convertToDatabaseProduct,
|
convertToDatabaseProduct,
|
||||||
convertToDatabaseSubscription,
|
convertToDatabaseSubscription,
|
||||||
|
RunMutationCtx,
|
||||||
RunQueryCtx,
|
RunQueryCtx,
|
||||||
} from "../component/util";
|
} from "../component/util";
|
||||||
|
import {
|
||||||
|
validateEvent,
|
||||||
|
WebhookVerificationError,
|
||||||
|
} from "@polar-sh/sdk/webhooks";
|
||||||
|
import {
|
||||||
|
WebhookSubscriptionCreatedPayload,
|
||||||
|
WebhookSubscriptionUpdatedPayload,
|
||||||
|
WebhookProductCreatedPayload,
|
||||||
|
WebhookProductUpdatedPayload,
|
||||||
|
} from "@polar-sh/sdk/models/components";
|
||||||
|
import { Doc } from "../component/_generated/dataModel";
|
||||||
|
import { Infer } from "convex/values";
|
||||||
|
import schema from "../component/schema";
|
||||||
|
|
||||||
export type EventType = (
|
export const subscriptionValidator = schema.tables.subscriptions.validator;
|
||||||
| WebhookOrderCreatedPayload$Outbound
|
export type Subscription = Infer<typeof subscriptionValidator>;
|
||||||
| WebhookSubscriptionCreatedPayload$Outbound
|
|
||||||
| WebhookSubscriptionUpdatedPayload$Outbound
|
|
||||||
| WebhookBenefitCreatedPayload$Outbound
|
|
||||||
| WebhookBenefitUpdatedPayload$Outbound
|
|
||||||
| WebhookProductCreatedPayload$Outbound
|
|
||||||
| WebhookProductUpdatedPayload$Outbound
|
|
||||||
| WebhookBenefitGrantCreatedPayload$Outbound
|
|
||||||
| WebhookBenefitGrantUpdatedPayload$Outbound
|
|
||||||
)["type"];
|
|
||||||
|
|
||||||
export type EventHandler = FunctionReference<
|
export type SubscriptionHandler = FunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
"internal",
|
"internal",
|
||||||
{ payload: unknown }
|
{ subscription: Subscription }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export class Polar {
|
export class Polar {
|
||||||
constructor(public component: ComponentApi) {}
|
public onScheduleCreated?: SubscriptionHandler;
|
||||||
|
constructor(
|
||||||
|
public component: ComponentApi,
|
||||||
|
options: {
|
||||||
|
onScheduleCreated?: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ subscription: WithoutSystemFields<Doc<"subscriptions">> }
|
||||||
|
>;
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
this.onScheduleCreated = options.onScheduleCreated;
|
||||||
|
}
|
||||||
listProducts(
|
listProducts(
|
||||||
ctx: RunQueryCtx,
|
ctx: RunQueryCtx,
|
||||||
{ includeArchived }: { includeArchived: boolean }
|
{ includeArchived }: { includeArchived: boolean }
|
||||||
@@ -67,30 +62,32 @@ export class Polar {
|
|||||||
getSubscription(ctx: RunQueryCtx, { id }: { id: string }) {
|
getSubscription(ctx: RunQueryCtx, { id }: { id: string }) {
|
||||||
return ctx.runQuery(this.component.lib.getSubscription, { id });
|
return ctx.runQuery(this.component.lib.getSubscription, { id });
|
||||||
}
|
}
|
||||||
getOrder(ctx: RunQueryCtx, { id }: { id: string }) {
|
|
||||||
return ctx.runQuery(this.component.lib.getOrder, { id });
|
|
||||||
}
|
|
||||||
getBenefit(ctx: RunQueryCtx, { id }: { id: string }) {
|
|
||||||
return ctx.runQuery(this.component.lib.getBenefit, { id });
|
|
||||||
}
|
|
||||||
getBenefitGrant(ctx: RunQueryCtx, { id }: { id: string }) {
|
|
||||||
return ctx.runQuery(this.component.lib.getBenefitGrant, { id });
|
|
||||||
}
|
|
||||||
getProduct(ctx: RunQueryCtx, { id }: { id: string }) {
|
getProduct(ctx: RunQueryCtx, { id }: { id: string }) {
|
||||||
return ctx.runQuery(this.component.lib.getProduct, { id });
|
return ctx.runQuery(this.component.lib.getProduct, { id });
|
||||||
}
|
}
|
||||||
listBenefits(ctx: RunQueryCtx) {
|
|
||||||
return ctx.runQuery(this.component.lib.listBenefits);
|
|
||||||
}
|
|
||||||
listUserBenefitGrants(ctx: RunQueryCtx, { userId }: { userId: string }) {
|
|
||||||
return ctx.runQuery(this.component.lib.listUserBenefitGrants, { userId });
|
|
||||||
}
|
|
||||||
registerRoutes(
|
registerRoutes(
|
||||||
http: HttpRouter,
|
http: HttpRouter,
|
||||||
{
|
{
|
||||||
path = "/polar/events",
|
path = "/polar/events",
|
||||||
eventCallback,
|
}: {
|
||||||
}: { eventCallback?: EventHandler; path?: string } = {}
|
path?: string;
|
||||||
|
onSubscriptionCreated?: (
|
||||||
|
ctx: RunMutationCtx,
|
||||||
|
event: WebhookSubscriptionCreatedPayload
|
||||||
|
) => Promise<void>;
|
||||||
|
onSubscriptionUpdated?: (
|
||||||
|
ctx: RunMutationCtx,
|
||||||
|
event: WebhookSubscriptionUpdatedPayload
|
||||||
|
) => Promise<void>;
|
||||||
|
onProductCreated?: (
|
||||||
|
ctx: RunMutationCtx,
|
||||||
|
event: WebhookProductCreatedPayload
|
||||||
|
) => Promise<void>;
|
||||||
|
onProductUpdated?: (
|
||||||
|
ctx: RunMutationCtx,
|
||||||
|
event: WebhookProductUpdatedPayload
|
||||||
|
) => Promise<void>;
|
||||||
|
} = {}
|
||||||
) {
|
) {
|
||||||
http.route({
|
http.route({
|
||||||
path,
|
path,
|
||||||
@@ -100,78 +97,45 @@ export class Polar {
|
|||||||
throw new Error("No body");
|
throw new Error("No body");
|
||||||
}
|
}
|
||||||
const body = await request.text();
|
const body = await request.text();
|
||||||
const wh = new Webhook(btoa(process.env.POLAR_WEBHOOK_SECRET!));
|
|
||||||
const headers = Object.fromEntries(request.headers.entries());
|
const headers = Object.fromEntries(request.headers.entries());
|
||||||
const payload = wh.verify(body, headers) as {
|
try {
|
||||||
type: EventType;
|
const event = validateEvent(
|
||||||
data: unknown;
|
body,
|
||||||
};
|
headers,
|
||||||
|
process.env["POLAR_WEBHOOK_SECRET"] ?? ""
|
||||||
switch (payload.type) {
|
);
|
||||||
case "order.created": {
|
switch (event.type) {
|
||||||
await ctx.runMutation(this.component.lib.updateOrder, {
|
case "subscription.created": {
|
||||||
order: convertToDatabaseOrder(
|
await ctx.runMutation(this.component.lib.createSubscription, {
|
||||||
WebhookOrderCreatedPayload$inboundSchema.parse(payload).data
|
subscription: convertToDatabaseSubscription(event.data),
|
||||||
),
|
callback:
|
||||||
});
|
this.onScheduleCreated &&
|
||||||
break;
|
(await createFunctionHandle(this.onScheduleCreated)),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "subscription.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateSubscription, {
|
||||||
|
subscription: convertToDatabaseSubscription(event.data),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "product.created":
|
||||||
|
case "product.updated": {
|
||||||
|
await ctx.runMutation(this.component.lib.updateProduct, {
|
||||||
|
product: convertToDatabaseProduct(event.data),
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case "subscription.created":
|
return new Response("Accepted", { status: 202 });
|
||||||
case "subscription.updated": {
|
} catch (error) {
|
||||||
const schema =
|
if (error instanceof WebhookVerificationError) {
|
||||||
payload.type === "subscription.created"
|
console.error(error);
|
||||||
? WebhookSubscriptionCreatedPayload$inboundSchema
|
return new Response("Forbidden", { status: 403 });
|
||||||
: WebhookSubscriptionUpdatedPayload$inboundSchema;
|
|
||||||
await ctx.runMutation(this.component.lib.updateSubscription, {
|
|
||||||
subscription: convertToDatabaseSubscription(
|
|
||||||
schema.parse(payload).data
|
|
||||||
),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "product.created":
|
|
||||||
case "product.updated": {
|
|
||||||
const schema =
|
|
||||||
payload.type === "product.created"
|
|
||||||
? WebhookProductCreatedPayload$inboundSchema
|
|
||||||
: WebhookProductUpdatedPayload$inboundSchema;
|
|
||||||
await ctx.runMutation(this.component.lib.updateProduct, {
|
|
||||||
product: convertToDatabaseProduct(schema.parse(payload).data),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "benefit.created":
|
|
||||||
case "benefit.updated": {
|
|
||||||
const schema =
|
|
||||||
payload.type === "benefit.created"
|
|
||||||
? WebhookBenefitCreatedPayload$inboundSchema
|
|
||||||
: WebhookBenefitUpdatedPayload$inboundSchema;
|
|
||||||
await ctx.runMutation(this.component.lib.updateBenefit, {
|
|
||||||
benefit: convertToDatabaseBenefit(schema.parse(payload).data),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "benefit_grant.created":
|
|
||||||
case "benefit_grant.updated": {
|
|
||||||
const schema =
|
|
||||||
payload.type === "benefit_grant.created"
|
|
||||||
? WebhookBenefitGrantCreatedPayload$inboundSchema
|
|
||||||
: WebhookBenefitGrantUpdatedPayload$inboundSchema;
|
|
||||||
await ctx.runMutation(this.component.lib.updateBenefitGrant, {
|
|
||||||
benefitGrant: convertToDatabaseBenefitGrant(
|
|
||||||
schema.parse(payload).data
|
|
||||||
),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventCallback) {
|
|
||||||
await ctx.runMutation(await createFunctionHandle(eventCallback), {
|
|
||||||
payload,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Response("OK", { status: 200 });
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
2
src/client/polyfill.ts
Normal file
2
src/client/polyfill.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import { Buffer as BufferPolyfill } from "buffer";
|
||||||
|
globalThis.Buffer = BufferPolyfill;
|
||||||
224
src/component/_generated/api.d.ts
vendored
224
src/component/_generated/api.d.ts
vendored
@@ -30,66 +30,80 @@ declare const fullApi: ApiFromModules<{
|
|||||||
}>;
|
}>;
|
||||||
export type Mounts = {
|
export type Mounts = {
|
||||||
lib: {
|
lib: {
|
||||||
getBenefit: FunctionReference<
|
createProduct: FunctionReference<
|
||||||
"query",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
{ id: string },
|
|
||||||
{
|
{
|
||||||
_creationTime: number;
|
product: {
|
||||||
_id: string;
|
createdAt: string;
|
||||||
createdAt: string;
|
description: string | null;
|
||||||
deletable: boolean;
|
id: string;
|
||||||
description: string;
|
isArchived: boolean;
|
||||||
id: string;
|
isRecurring: boolean;
|
||||||
modifiedAt: string | null;
|
medias: Array<{
|
||||||
organizationId: string;
|
checksumEtag: string | null;
|
||||||
properties: Record<string, any>;
|
checksumSha256Base64: string | null;
|
||||||
selectable: boolean;
|
checksumSha256Hex: string | null;
|
||||||
type?: string;
|
createdAt: string;
|
||||||
} | null
|
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
|
||||||
>;
|
>;
|
||||||
getBenefitGrant: FunctionReference<
|
createSubscription: FunctionReference<
|
||||||
"query",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
{ id: string },
|
|
||||||
{
|
{
|
||||||
_creationTime: number;
|
callback?: string;
|
||||||
_id: string;
|
subscription: {
|
||||||
benefitId: string;
|
amount: number | null;
|
||||||
createdAt: string;
|
cancelAtPeriodEnd: boolean;
|
||||||
grantedAt: string | null;
|
checkoutId: string | null;
|
||||||
id: string;
|
createdAt: string;
|
||||||
isGranted: boolean;
|
currency: string | null;
|
||||||
isRevoked: boolean;
|
currentPeriodEnd: string | null;
|
||||||
modifiedAt: string | null;
|
currentPeriodStart: string;
|
||||||
orderId: string | null;
|
endedAt: string | null;
|
||||||
properties: Record<string, any>;
|
id: string;
|
||||||
revokedAt: string | null;
|
metadata: Record<string, any>;
|
||||||
subscriptionId: string | null;
|
modifiedAt: string | null;
|
||||||
userId: string;
|
priceId: string;
|
||||||
} | null
|
productId: string;
|
||||||
>;
|
recurringInterval: string;
|
||||||
getOrder: FunctionReference<
|
startedAt: string | null;
|
||||||
"query",
|
status: string;
|
||||||
"public",
|
userId: string;
|
||||||
{ id: string },
|
};
|
||||||
{
|
},
|
||||||
_creationTime: number;
|
any
|
||||||
_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<
|
getProduct: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
@@ -165,24 +179,6 @@ export type Mounts = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
} | null
|
} | null
|
||||||
>;
|
>;
|
||||||
listBenefits: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"public",
|
|
||||||
{},
|
|
||||||
Array<{
|
|
||||||
_creationTime: number;
|
|
||||||
_id: string;
|
|
||||||
createdAt: string;
|
|
||||||
deletable: boolean;
|
|
||||||
description: string;
|
|
||||||
id: string;
|
|
||||||
modifiedAt: string | null;
|
|
||||||
organizationId: string;
|
|
||||||
properties: Record<string, any>;
|
|
||||||
selectable: boolean;
|
|
||||||
type?: string;
|
|
||||||
}>
|
|
||||||
>;
|
|
||||||
listProducts: FunctionReference<
|
listProducts: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"public",
|
"public",
|
||||||
@@ -231,27 +227,6 @@ export type Mounts = {
|
|||||||
}>;
|
}>;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
listUserBenefitGrants: FunctionReference<
|
|
||||||
"query",
|
|
||||||
"public",
|
|
||||||
{ userId: string },
|
|
||||||
Array<{
|
|
||||||
_creationTime: number;
|
|
||||||
_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;
|
|
||||||
}>
|
|
||||||
>;
|
|
||||||
listUserSubscriptions: FunctionReference<
|
listUserSubscriptions: FunctionReference<
|
||||||
"query",
|
"query",
|
||||||
"public",
|
"public",
|
||||||
@@ -321,67 +296,6 @@ export type Mounts = {
|
|||||||
userId: string;
|
userId: string;
|
||||||
}>
|
}>
|
||||||
>;
|
>;
|
||||||
updateBenefit: 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
|
|
||||||
>;
|
|
||||||
updateBenefitGrant: 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
|
|
||||||
>;
|
|
||||||
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<
|
updateProduct: FunctionReference<
|
||||||
"mutation",
|
"mutation",
|
||||||
"public",
|
"public",
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { v } from "convex/values";
|
import { v, VString } from "convex/values";
|
||||||
import { mutation, query } from "./_generated/server";
|
import { mutation, query } from "./_generated/server";
|
||||||
import schema from "./schema";
|
import schema from "./schema";
|
||||||
import { asyncMap } from "convex-helpers";
|
import { asyncMap } from "convex-helpers";
|
||||||
|
import { FunctionHandle, WithoutSystemFields } from "convex/server";
|
||||||
|
import { Doc } from "./_generated/dataModel";
|
||||||
|
|
||||||
export const getSubscription = query({
|
export const getSubscription = query({
|
||||||
args: {
|
args: {
|
||||||
@@ -23,26 +25,6 @@ export const getSubscription = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getOrder = query({
|
|
||||||
args: {
|
|
||||||
id: v.id("orders"),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
...schema.tables.orders.validator.fields,
|
|
||||||
_id: v.id("orders"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
}),
|
|
||||||
v.null()
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
return ctx.db
|
|
||||||
.query("orders")
|
|
||||||
.withIndex("id", (q) => q.eq("id", args.id))
|
|
||||||
.unique();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getProduct = query({
|
export const getProduct = query({
|
||||||
args: {
|
args: {
|
||||||
id: v.id("products"),
|
id: v.id("products"),
|
||||||
@@ -126,20 +108,21 @@ export const listProducts = query({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateOrder = mutation({
|
type Subscription = WithoutSystemFields<Doc<"subscriptions">>;
|
||||||
|
const subscriptionCallbackValidator = v.string() as VString<
|
||||||
|
FunctionHandle<"mutation", { subscription: Subscription }>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const createSubscription = mutation({
|
||||||
args: {
|
args: {
|
||||||
order: schema.tables.orders.validator,
|
subscription: schema.tables.subscriptions.validator,
|
||||||
|
callback: v.optional(subscriptionCallbackValidator),
|
||||||
},
|
},
|
||||||
handler: async (ctx, args) => {
|
handler: async (ctx, args) => {
|
||||||
const existingOrder = await ctx.db
|
await ctx.db.insert("subscriptions", args.subscription);
|
||||||
.query("orders")
|
if (args.callback) {
|
||||||
.withIndex("id", (q) => q.eq("id", args.order.id))
|
await ctx.runMutation(args.callback, { subscription: args.subscription });
|
||||||
.unique();
|
|
||||||
if (existingOrder) {
|
|
||||||
await ctx.db.patch(existingOrder._id, args.order);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await ctx.db.insert("orders", args.order);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,11 +135,19 @@ export const updateSubscription = mutation({
|
|||||||
.query("subscriptions")
|
.query("subscriptions")
|
||||||
.withIndex("id", (q) => q.eq("id", args.subscription.id))
|
.withIndex("id", (q) => q.eq("id", args.subscription.id))
|
||||||
.unique();
|
.unique();
|
||||||
if (existingSubscription) {
|
if (!existingSubscription) {
|
||||||
await ctx.db.patch(existingSubscription._id, args.subscription);
|
throw new Error(`Subscription not found: ${args.subscription.id}`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await ctx.db.insert("subscriptions", args.subscription);
|
await ctx.db.patch(existingSubscription._id, args.subscription);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createProduct = mutation({
|
||||||
|
args: {
|
||||||
|
product: schema.tables.products.validator,
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
await ctx.db.insert("products", args.product);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,117 +160,9 @@ export const updateProduct = mutation({
|
|||||||
.query("products")
|
.query("products")
|
||||||
.withIndex("id", (q) => q.eq("id", args.product.id))
|
.withIndex("id", (q) => q.eq("id", args.product.id))
|
||||||
.unique();
|
.unique();
|
||||||
if (existingProduct) {
|
if (!existingProduct) {
|
||||||
await ctx.db.patch(existingProduct._id, args.product);
|
throw new Error(`Product not found: ${args.product.id}`);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
await ctx.db.insert("products", args.product);
|
await ctx.db.patch(existingProduct._id, args.product);
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ctx.db.insert("benefits", args.benefit);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getBenefit = query({
|
|
||||||
args: {
|
|
||||||
id: v.id("benefits"),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
...schema.tables.benefits.validator.fields,
|
|
||||||
_id: v.id("benefits"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
}),
|
|
||||||
v.null()
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
return ctx.db
|
|
||||||
.query("benefits")
|
|
||||||
.withIndex("id", (q) => q.eq("id", args.id))
|
|
||||||
.unique();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const listBenefits = query({
|
|
||||||
args: {},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
...schema.tables.benefits.validator.fields,
|
|
||||||
_id: v.id("benefits"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx) => {
|
|
||||||
return ctx.db.query("benefits").collect();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await ctx.db.insert("benefitGrants", args.benefitGrant);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getBenefitGrant = query({
|
|
||||||
args: {
|
|
||||||
id: v.id("benefitGrants"),
|
|
||||||
},
|
|
||||||
returns: v.union(
|
|
||||||
v.object({
|
|
||||||
...schema.tables.benefitGrants.validator.fields,
|
|
||||||
_id: v.id("benefitGrants"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
}),
|
|
||||||
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: {
|
|
||||||
userId: v.string(),
|
|
||||||
},
|
|
||||||
returns: v.array(
|
|
||||||
v.object({
|
|
||||||
...schema.tables.benefitGrants.validator.fields,
|
|
||||||
_id: v.id("benefitGrants"),
|
|
||||||
_creationTime: v.number(),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
handler: async (ctx, args) => {
|
|
||||||
return ctx.db
|
|
||||||
.query("benefitGrants")
|
|
||||||
.withIndex("userId", (q) => q.eq("userId", args.userId))
|
|
||||||
.collect();
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,50 +3,6 @@ import { v } from "convex/values";
|
|||||||
|
|
||||||
export default defineSchema(
|
export default defineSchema(
|
||||||
{
|
{
|
||||||
benefits: defineTable({
|
|
||||||
id: v.string(),
|
|
||||||
createdAt: v.string(),
|
|
||||||
modifiedAt: v.union(v.string(), v.null()),
|
|
||||||
organizationId: v.string(),
|
|
||||||
type: v.optional(v.string()),
|
|
||||||
description: v.string(),
|
|
||||||
selectable: v.boolean(),
|
|
||||||
deletable: v.boolean(),
|
|
||||||
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("id", ["id"])
|
|
||||||
.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({
|
products: defineTable({
|
||||||
id: v.string(),
|
id: v.string(),
|
||||||
createdAt: v.string(),
|
createdAt: v.string(),
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import type { GenericQueryCtx, WithoutSystemFields } from "convex/server";
|
import type {
|
||||||
|
FunctionHandle,
|
||||||
|
FunctionType,
|
||||||
|
GenericQueryCtx,
|
||||||
|
WithoutSystemFields,
|
||||||
|
} from "convex/server";
|
||||||
import type { Expand, FunctionReference } from "convex/server";
|
import type { Expand, FunctionReference } from "convex/server";
|
||||||
|
|
||||||
import type {
|
import type { Product, Subscription } from "@polar-sh/sdk/models/components";
|
||||||
Benefit,
|
|
||||||
BenefitGrant,
|
|
||||||
Order,
|
|
||||||
Product,
|
|
||||||
Subscription,
|
|
||||||
} from "@polar-sh/sdk/models/components";
|
|
||||||
import type { GenericMutationCtx } from "convex/server";
|
import type { GenericMutationCtx } from "convex/server";
|
||||||
import type { GenericDataModel } from "convex/server";
|
import type { GenericDataModel } from "convex/server";
|
||||||
import type { GenericActionCtx } from "convex/server";
|
import type { GenericActionCtx } from "convex/server";
|
||||||
@@ -25,13 +24,16 @@ export type RunActionCtx = {
|
|||||||
runAction: GenericActionCtx<GenericDataModel>["runAction"];
|
runAction: GenericActionCtx<GenericDataModel>["runAction"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type OpaqueIds<T> = T extends GenericId<infer _T>
|
export type OpaqueIds<T> =
|
||||||
? string
|
T extends GenericId<infer _T>
|
||||||
: T extends (infer U)[]
|
? string
|
||||||
? OpaqueIds<U>[]
|
: T extends FunctionHandle<FunctionType>
|
||||||
: T extends object
|
? string
|
||||||
? { [K in keyof T]: OpaqueIds<T[K]> }
|
: T extends (infer U)[]
|
||||||
: T;
|
? OpaqueIds<U>[]
|
||||||
|
: T extends object
|
||||||
|
? { [K in keyof T]: OpaqueIds<T[K]> }
|
||||||
|
: T;
|
||||||
|
|
||||||
export type UseApi<API> = Expand<{
|
export type UseApi<API> = Expand<{
|
||||||
[mod in keyof API]: API[mod] extends FunctionReference<
|
[mod in keyof API]: API[mod] extends FunctionReference<
|
||||||
@@ -53,28 +55,8 @@ export type UseApi<API> = Expand<{
|
|||||||
|
|
||||||
export type ComponentApi = UseApi<typeof api>;
|
export type ComponentApi = UseApi<typeof api>;
|
||||||
|
|
||||||
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 = (
|
export const convertToDatabaseSubscription = (
|
||||||
subscription: Subscription,
|
subscription: Subscription
|
||||||
): WithoutSystemFields<Doc<"subscriptions">> => {
|
): WithoutSystemFields<Doc<"subscriptions">> => {
|
||||||
return {
|
return {
|
||||||
id: subscription.id,
|
id: subscription.id,
|
||||||
@@ -98,7 +80,7 @@ export const convertToDatabaseSubscription = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const convertToDatabaseProduct = (
|
export const convertToDatabaseProduct = (
|
||||||
product: Product,
|
product: Product
|
||||||
): WithoutSystemFields<Doc<"products">> => {
|
): WithoutSystemFields<Doc<"products">> => {
|
||||||
return {
|
return {
|
||||||
id: product.id,
|
id: product.id,
|
||||||
@@ -151,38 +133,3 @@ export const convertToDatabaseProduct = (
|
|||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user