feat(cli): add polar as better-auth plugin (#578)

This commit is contained in:
Aman Varshney
2025-09-16 17:53:44 +05:30
committed by GitHub
parent 3f22373cc3
commit ba3d62b6b9
77 changed files with 1221 additions and 308 deletions

View File

@@ -4,6 +4,10 @@ import { prismaAdapter } from "better-auth/adapters/prisma";
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
import { expo } from "@better-auth/expo";
{{/if}}
{{#if (eq payments "polar")}}
import { polar, checkout, portal } from "@polar-sh/better-auth";
import { polarClient } from "./payments";
{{/if}}
import prisma from "../db";
export const auth = betterAuth<BetterAuthOptions>({
@@ -28,9 +32,35 @@ export const auth = betterAuth<BetterAuthOptions>({
secure: true,
httpOnly: true,
},
}
},
{{#if (eq payments "polar")}}
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
enableCustomerPortal: true,
use: [
checkout({
products: [
{
productId: "your-product-id",
slug: "pro",
},
],
successUrl: process.env.POLAR_SUCCESS_URL,
authenticatedUsersOnly: true,
}),
portal(),
],
}),
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
expo(),
{{/if}}
],
{{else}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
, plugins: [expo()]
plugins: [expo()],
{{/if}}
{{/if}}
});
{{/if}}
@@ -42,6 +72,10 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
import { expo } from "@better-auth/expo";
{{/if}}
{{#if (eq payments "polar")}}
import { polar, checkout, portal } from "@polar-sh/better-auth";
import { polarClient } from "./payments";
{{/if}}
import { db } from "../db";
import * as schema from "../db/schema/auth";
@@ -68,9 +102,35 @@ export const auth = betterAuth<BetterAuthOptions>({
httpOnly: true,
},
},
{{#if (eq payments "polar")}}
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
enableCustomerPortal: true,
use: [
checkout({
products: [
{
productId: "your-product-id",
slug: "pro",
},
],
successUrl: process.env.POLAR_SUCCESS_URL,
authenticatedUsersOnly: true,
}),
portal(),
],
}),
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
expo(),
{{/if}}
],
{{else}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
plugins: [expo()],
{{/if}}
{{/if}}
});
{{/if}}
@@ -80,6 +140,10 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
import { expo } from "@better-auth/expo";
{{/if}}
{{#if (eq payments "polar")}}
import { polar, checkout, portal } from "@polar-sh/better-auth";
import { polarClient } from "./payments";
{{/if}}
import { db } from "../db";
import * as schema from "../db/schema/auth";
import { env } from "cloudflare:workers";
@@ -109,9 +173,32 @@ export const auth = betterAuth<BetterAuthOptions>({
httpOnly: true,
},
},
{{#if (eq payments "polar")}}
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
enableCustomerPortal: true,
use: [
checkout({
products: [
{
productId: "your-product-id",
slug: "pro",
},
],
successUrl: env.POLAR_SUCCESS_URL,
authenticatedUsersOnly: true,
}),
portal(),
],
}),
],
{{else}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
plugins: [expo()],
{{/if}}
{{/if}}
});
{{/if}}
{{/if}}
@@ -122,6 +209,10 @@ import { mongodbAdapter } from "better-auth/adapters/mongodb";
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
import { expo } from "@better-auth/expo";
{{/if}}
{{#if (eq payments "polar")}}
import { polar, checkout, portal } from "@polar-sh/better-auth";
import { polarClient } from "./payments";
{{/if}}
import { client } from "../db";
export const auth = betterAuth<BetterAuthOptions>({
@@ -141,9 +232,35 @@ export const auth = betterAuth<BetterAuthOptions>({
secure: true,
httpOnly: true,
},
}
},
{{#if (eq payments "polar")}}
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
enableCustomerPortal: true,
use: [
checkout({
products: [
{
productId: "your-product-id",
slug: "pro",
},
],
successUrl: process.env.POLAR_SUCCESS_URL,
authenticatedUsersOnly: true,
}),
portal(),
],
}),
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
expo(),
{{/if}}
],
{{else}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
, plugins: [expo()]
plugins: [expo()],
{{/if}}
{{/if}}
});
{{/if}}
@@ -153,6 +270,10 @@ import { betterAuth, type BetterAuthOptions } from "better-auth";
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
import { expo } from "@better-auth/expo";
{{/if}}
{{#if (eq payments "polar")}}
import { polar, checkout, portal } from "@polar-sh/better-auth";
import { polarClient } from "./payments";
{{/if}}
export const auth = betterAuth<BetterAuthOptions>({
database: "", // Invalid configuration
@@ -171,9 +292,35 @@ export const auth = betterAuth<BetterAuthOptions>({
secure: true,
httpOnly: true,
},
}
},
{{#if (eq payments "polar")}}
plugins: [
polar({
client: polarClient,
createCustomerOnSignUp: true,
enableCustomerPortal: true,
use: [
checkout({
products: [
{
productId: "your-product-id",
slug: "pro",
},
],
successUrl: process.env.POLAR_SUCCESS_URL,
authenticatedUsersOnly: true,
}),
portal(),
],
}),
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
expo(),
{{/if}}
],
{{else}}
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
, plugins: [expo()]
plugins: [expo()],
{{/if}}
{{/if}}
});
{{/if}}

View File

@@ -11,9 +11,28 @@ definePageMeta({
const { $orpc } = useNuxtApp()
const session = $authClient.useSession()
{{#if (eq payments "polar")}}
const customerState = ref<any>(null)
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery($orpc.privateData.queryOptions())
const privateData = useQuery({
...$orpc.privateData.queryOptions(),
enabled: computed(() => !!session.value?.data?.user)
})
{{/if}}
{{#if (eq payments "polar")}}
onMounted(async () => {
if (session.value?.data) {
const { data } = await $authClient.customer.state()
customerState.value = data
}
})
const hasProSubscription = computed(() =>
customerState.value?.activeSubscriptions?.length! > 0
)
{{/if}}
</script>
@@ -27,7 +46,22 @@ const privateData = useQuery($orpc.privateData.queryOptions())
{{#if (eq api "orpc")}}
<div v-if="privateData.status.value === 'pending'">Loading private data...</div>
<div v-else-if="privateData.status.value === 'error'">Error loading private data: \{{ privateData.error.value?.message }}</div>
<p v-else-if="privateData.data.value">Private Data: \{{ privateData.data.value.message }}</p>
<p v-else-if="privateData.data.value">API: \{{ privateData.data.value.message }}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p class="mb-2">Plan: \{{ hasProSubscription ? "Pro" : "Free" }}</p>
<UButton
v-if="hasProSubscription"
@click="() => { $authClient.customer.portal() }"
>
Manage Subscription
</UButton>
<UButton
v-else
@click="() => { $authClient.checkout({ slug: 'pro' }) }"
>
Upgrade to Pro
</UButton>
{{/if}}
</div>
</template>

View File

@@ -1,16 +0,0 @@
import { createAuthClient } from "better-auth/vue";
export default defineNuxtPlugin(nuxtApp => {
const config = useRuntimeConfig()
const serverUrl = config.public.serverURL
const authClient = createAuthClient({
baseURL: serverUrl
})
return {
provide: {
authClient: authClient
}
}
})

View File

@@ -0,0 +1,22 @@
import { createAuthClient } from "better-auth/vue";
{{#if (eq payments "polar")}}
import { polarClient } from "@polar-sh/better-auth";
{{/if}}
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const serverUrl = config.public.serverURL;
const authClient = createAuthClient({
baseURL: serverUrl,
{{#if (eq payments "polar")}}
plugins: [polarClient()],
{{/if}}
});
return {
provide: {
authClient: authClient,
},
};
});

View File

@@ -1,4 +1,7 @@
import { createAuthClient } from "better-auth/react";
{{#if (eq payments "polar")}}
import { polarClient } from "@polar-sh/better-auth";
{{/if}}
export const authClient = createAuthClient({
baseURL:
@@ -7,4 +10,7 @@ export const authClient = createAuthClient({
{{else}}
import.meta.env.VITE_SERVER_URL,
{{/if}}
{{#if (eq payments "polar")}}
plugins: [polarClient()]
{{/if}}
});

View File

@@ -0,0 +1,58 @@
"use client";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
{{/if}}
export default function Dashboard({
{{#if (eq payments "polar")}}
customerState,
{{/if}}
session
}: {
{{#if (eq payments "polar")}}
customerState: ReturnType<typeof authClient.customer.state>;
{{/if}}
session: typeof authClient.$Infer.Session;
}) {
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq payments "polar")}}
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0;
console.log("Active subscriptions:", customerState?.activeSubscriptions);
{{/if}}
return (
<>
{{#if (eq api "orpc")}}
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq api "trpc")}}
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
{hasProSubscription ? (
<Button onClick={async () => await authClient.customer.portal()}>
Manage Subscription
</Button>
) : (
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
Upgrade to Pro
</Button>
)}
{{/if}}
</>
);
}

View File

@@ -1,47 +1,37 @@
"use client"
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
{{/if}}
{{#if (eq api "trpc")}}
import { useQuery } from "@tanstack/react-query";
import { trpc } from "@/utils/trpc";
{{/if}}
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { redirect } from "next/navigation";
import Dashboard from "./dashboard";
import { headers } from "next/headers";
export default function Dashboard() {
const router = useRouter();
const { data: session, isPending } = authClient.useSession();
export default async function DashboardPage() {
const session = await authClient.getSession({
fetchOptions: {
headers: await headers()
}
});
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
if (!session.data) {
redirect("/login");
}
useEffect(() => {
if (!session && !isPending) {
router.push("/login");
}
}, [session, isPending]);
{{#if (eq payments "polar")}}
const { data: customerState, error } = await authClient.customer.state({
fetchOptions: {
headers: await headers()
}
});
{{/if}}
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
{{#if (eq api "trpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session.data.user.name}</p>
<Dashboard
session={session.data}
{{#if (eq payments "polar")}}
customerState={customerState}
{{/if}}
/>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
@@ -8,12 +9,15 @@ import { trpc } from "@/utils/trpc";
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { useEffect } from "react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router";
export default function Dashboard() {
const { data: session, isPending } = authClient.useSession();
const navigate = useNavigate();
{{#if (eq payments "polar")}}
const [customerState, setCustomerState] = useState<any>(null);
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
@@ -26,18 +30,48 @@ export default function Dashboard() {
if (!session && !isPending) {
navigate("/login");
}
}, [session, isPending]);
}, [session, isPending, navigate]);
{{#if (eq payments "polar")}}
useEffect(() => {
async function fetchCustomerState() {
if (session) {
const { data } = await authClient.customer.state();
setCustomerState(data);
}
}
fetchCustomerState();
}, [session]);
{{/if}}
if (isPending) {
return <div>Loading...</div>;
}
{{#if (eq payments "polar")}}
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0;
console.log("Active subscriptions:", customerState?.activeSubscriptions);
{{/if}}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
{hasProSubscription ? (
<Button onClick={async () => await authClient.customer.portal()}>
Manage Subscription
</Button>
) : (
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
Upgrade to Pro
</Button>
)}
{{/if}}
</div>
);

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
{{#if (eq api "orpc")}}
import { orpc } from "@/utils/orpc";
@@ -8,44 +9,61 @@ import { trpc } from "@/utils/trpc";
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
component: RouteComponent,
beforeLoad: async () => {
const session = await authClient.getSession();
if (!session.data) {
redirect({
to: "/login",
throw: true
});
}
{{#if (eq payments "polar")}}
const {data: customerState} = await authClient.customer.state()
return { session, customerState };
{{else}}
return { session };
{{/if}}
}
});
function RouteComponent() {
const { data: session, isPending } = authClient.useSession();
const { session{{#if (eq payments "polar")}}, customerState{{/if}} } = Route.useRouteContext();
const navigate = Route.useNavigate();
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq payments "polar")}}
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0
console.log("Active subscriptions:", customerState?.activeSubscriptions)
{{/if}}
useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/login",
});
}
}, [session, isPending]);
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session.data?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
{hasProSubscription ? (
<Button onClick={async () => await authClient.customer.portal()}>
Manage Subscription
</Button>
) : (
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
Upgrade to Pro
</Button>
)}
{{/if}}
</div>
);
}

View File

@@ -1,3 +1,4 @@
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth-client";
{{#if (eq api "trpc")}}
import { useTRPC } from "@/utils/trpc";
@@ -7,48 +8,62 @@ import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/utils/orpc";
import { useQuery } from "@tanstack/react-query";
{{/if}}
import { createFileRoute } from "@tanstack/react-router";
import { useEffect } from "react";
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
component: RouteComponent,
beforeLoad: async () => {
const session = await authClient.getSession();
if (!session.data) {
redirect({
to: "/login",
throw: true
});
}
{{#if (eq payments "polar")}}
const {data: customerState} = await authClient.customer.state()
return { session, customerState };
{{else}}
return { session };
{{/if}}
}
});
function RouteComponent() {
const navigate = Route.useNavigate();
{{#if (eq api "trpc")}}
const trpc = useTRPC();
{{/if}}
{{#if (eq api "orpc")}}
{{/if}}
const { data: session, isPending } = authClient.useSession();
const { session{{#if (eq payments "polar")}}, customerState{{/if}} } = Route.useRouteContext();
{{#if (eq api "trpc")}}
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "trpc")}}
const trpc = useTRPC();
const privateData = useQuery(trpc.privateData.queryOptions());
{{/if}}
{{#if (eq api "orpc")}}
const privateData = useQuery(orpc.privateData.queryOptions());
{{/if}}
useEffect(() => {
if (!session && !isPending) {
navigate({
to: "/login",
});
}
}, [session, isPending]);
{{#if (eq payments "polar")}}
const hasProSubscription = customerState?.activeSubscriptions?.length! > 0
console.log("Active subscriptions:", customerState?.activeSubscriptions)
{{/if}}
if (isPending) {
return <div>Loading...</div>;
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</div>
);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session.data?.user.name}</p>
{{#if ( or (eq api "orpc") (eq api "trpc"))}}
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {hasProSubscription ? "Pro" : "Free"}</p>
{hasProSubscription ? (
<Button onClick={async () => await authClient.customer.portal()}>
Manage Subscription
</Button>
) : (
<Button onClick={async () => await authClient.checkout({ slug: "pro" })}>
Upgrade to Pro
</Button>
)}
{{/if}}
</div>
);
}

View File

@@ -1,5 +0,0 @@
import { createAuthClient } from "better-auth/solid";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SERVER_URL,
});

View File

@@ -0,0 +1,11 @@
import { createAuthClient } from "better-auth/solid";
{{#if (eq payments "polar")}}
import { polarClient } from "@polar-sh/better-auth";
{{/if}}
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_SERVER_URL,
{{#if (eq payments "polar")}}
plugins: [polarClient()]
{{/if}}
});

View File

@@ -3,42 +3,65 @@ import { authClient } from "@/lib/auth-client";
import { orpc } from "@/utils/orpc";
import { useQuery } from "@tanstack/solid-query";
{{/if}}
import { createFileRoute } from "@tanstack/solid-router";
import { createEffect, Show } from "solid-js";
import { createFileRoute, redirect } from "@tanstack/solid-router";
export const Route = createFileRoute("/dashboard")({
component: RouteComponent,
component: RouteComponent,
beforeLoad: async () => {
const session = await authClient.getSession();
if (!session.data) {
redirect({
to: "/login",
throw: true,
});
}
{{#if (eq payments "polar")}}
const { data: customerState } = await authClient.customer.state();
return { session, customerState };
{{else}}
return { session };
{{/if}}
},
});
function RouteComponent() {
const session = authClient.useSession();
const navigate = Route.useNavigate();
const context = Route.useRouteContext();
{{#if (eq api "orpc")}}
const privateData = useQuery(() => orpc.privateData.queryOptions());
{{/if}}
const session = context().session;
{{#if (eq payments "polar")}}
const customerState = context().customerState;
{{/if}}
createEffect(() => {
if (!session().data && !session().isPending) {
navigate({
to: "/login",
});
}
});
{{#if (eq api "orpc")}}
const privateData = useQuery(() => orpc.privateData.queryOptions());
{{/if}}
return (
<div>
<Show when={session().isPending}>
<div>Loading...</div>
</Show>
{{#if (eq payments "polar")}}
const hasProSubscription = () =>
customerState?.activeSubscriptions?.length! > 0;
{{/if}}
<Show when={!session().isPending && session().data}>
<h1>Dashboard</h1>
<p>Welcome {session().data?.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {privateData.data?.message}</p>
{{/if}}
</Show>
</div>
);
return (
<div>
<h1>Dashboard</h1>
<p>Welcome {session.data?.user.name}</p>
{{#if (eq api "orpc")}}
<p>API: {privateData.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {hasProSubscription() ? "Pro" : "Free"}</p>
{hasProSubscription() ? (
<button onClick={async () => await authClient.customer.portal()}>
Manage Subscription
</button>
) : (
<button
onClick={async () => await authClient.checkout({ slug: "pro" })}
>
Upgrade to Pro
</button>
)}
{{/if}}
</div>
);
}

View File

@@ -1,6 +1,12 @@
import { PUBLIC_SERVER_URL } from "$env/static/public";
import { createAuthClient } from "better-auth/svelte";
{{#if (eq payments "polar")}}
import { polarClient } from "@polar-sh/better-auth";
{{/if}}
export const authClient = createAuthClient({
baseURL: PUBLIC_SERVER_URL,
{{#if (eq payments "polar")}}
plugins: [polarClient()]
{{/if}}
});

View File

@@ -6,7 +6,9 @@
import { orpc } from '$lib/orpc';
import { createQuery } from '@tanstack/svelte-query';
{{/if}}
import { get } from 'svelte/store';
{{#if (eq payments "polar")}}
let customerState: any = null;
{{/if}}
const sessionQuery = authClient.useSession();
@@ -15,10 +17,17 @@
{{/if}}
onMount(() => {
const { data: session, isPending } = get(sessionQuery);
const { data: session, isPending } = $sessionQuery;
if (!session && !isPending) {
goto('/login');
}
{{#if (eq payments "polar")}}
if (session) {
authClient.customer.state().then(({ data }) => {
customerState = data;
});
}
{{/if}}
});
</script>
@@ -30,7 +39,19 @@
<h1>Dashboard</h1>
<p>Welcome {$sessionQuery.data.user.name}</p>
{{#if (eq api "orpc")}}
<p>privateData: {$privateDataQuery.data?.message}</p>
<p>API: {$privateDataQuery.data?.message}</p>
{{/if}}
{{#if (eq payments "polar")}}
<p>Plan: {customerState?.activeSubscriptions?.length > 0 ? "Pro" : "Free"}</p>
{#if customerState?.activeSubscriptions?.length > 0}
<button onclick={async () => await authClient.customer.portal()}>
Manage Subscription
</button>
{:else}
<button onclick={async () => await authClient.checkout({ slug: "pro" })}>
Upgrade to Pro
</button>
{/if}
{{/if}}
</div>
{/if}

View File

@@ -17,7 +17,7 @@
],
{{/if}}
"devDependencies": {
"tsdown": "^0.14.1",
"tsdown": "^0.15.1",
"typescript": "^5.8.2"
}
}

View File

@@ -1,2 +1,6 @@
[install]
{{#if (includes frontend "nuxt")}}
# linker = "isolated" # Commented out for Nuxt compatibility
{{else}}
linker = "isolated"
{{/if}}

View File

@@ -17,6 +17,10 @@ import appCss from "../index.css?url";
import type { QueryClient } from "@tanstack/react-query";
import type { ConvexQueryClient } from "@convex-dev/react-query";
import type { ConvexReactClient } from "convex/react";
{{else}}
{{#if (or (eq api "trpc") (eq api "orpc"))}}
import type { QueryClient } from "@tanstack/react-query";
{{/if}}
{{/if}}
import Loader from "@/components/loader";

View File

@@ -9,18 +9,18 @@
"test": "vitest run"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.6",
"@tanstack/router-plugin": "^1.109.2",
"@tanstack/solid-form": "^1.9.0",
"@tanstack/solid-router": "^1.110.0",
"lucide-solid": "^0.507.0",
"solid-js": "^1.9.4",
"tailwindcss": "^4.0.6",
"@tailwindcss/vite": "^4.1.13",
"@tanstack/router-plugin": "^1.131.44",
"@tanstack/solid-form": "^1.20.0",
"@tanstack/solid-router": "^1.131.44",
"lucide-solid": "^0.544.0",
"solid-js": "^1.9.9",
"tailwindcss": "^4.1.13",
"zod": "^4.0.2"
},
"devDependencies": {
"typescript": "^5.7.2",
"vite": "^7.0.2",
"vite-plugin-solid": "^2.11.2"
"typescript": "^5.9.2",
"vite": "^7.1.5",
"vite-plugin-solid": "^2.11.8"
}
}

View File

@@ -0,0 +1,6 @@
import { Polar } from "@polar-sh/sdk";
export const polarClient = new Polar({
accessToken: process.env.POLAR_ACCESS_TOKEN,
server: "sandbox",
});

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
const route = useRoute()
const checkout_id = route.query.checkout_id as string
</script>
<template>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-4">Payment Successful!</h1>
<p v-if="checkout_id">Checkout ID: \{{ checkout_id }}</p>
</div>
</template>

View File

@@ -0,0 +1,15 @@
export default async function SuccessPage({
searchParams,
}: {
searchParams: Promise<{ checkout_id: string }>
}) {
const params = await searchParams;
const checkout_id = params.checkout_id;
return (
<div className="px-4 py-8">
<h1>Payment Successful!</h1>
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { useSearchParams } from "react-router";
export default function SuccessPage() {
const [searchParams] = useSearchParams();
const checkout_id = searchParams.get("checkout_id");
return (
<div className="container mx-auto px-4 py-8">
<h1>Payment Successful!</h1>
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { createFileRoute, useSearch } from "@tanstack/react-router";
export const Route = createFileRoute("/success")({
component: SuccessPage,
validateSearch: (search) => ({
checkout_id: search.checkout_id as string,
}),
});
function SuccessPage() {
const { checkout_id } = useSearch({ from: "/success" });
return (
<div className="container mx-auto px-4 py-8">
<h1>Payment Successful!</h1>
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { createFileRoute, useSearch } from "@tanstack/react-router";
export const Route = createFileRoute("/success")({
component: SuccessPage,
validateSearch: (search) => ({
checkout_id: search.checkout_id as string,
}),
});
function SuccessPage() {
const { checkout_id } = useSearch({ from: "/success" });
return (
<div className="container mx-auto px-4 py-8">
<h1>Payment Successful!</h1>
{checkout_id && <p>Checkout ID: {checkout_id}</p>}
</div>
);
}

View File

@@ -0,0 +1,23 @@
import { createFileRoute } from "@tanstack/solid-router";
import { Show } from "solid-js";
export const Route = createFileRoute("/success")({
component: SuccessPage,
validateSearch: (search) => ({
checkout_id: search.checkout_id as string,
}),
});
function SuccessPage() {
const searchParams = Route.useSearch();
const checkout_id = searchParams().checkout_id;
return (
<div class="container mx-auto px-4 py-8">
<h1>Payment Successful!</h1>
<Show when={checkout_id}>
<p>Checkout ID: {checkout_id}</p>
</Show>
</div>
);
}

View File

@@ -0,0 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
const checkout_id = $page.url.searchParams.get('checkout_id');
</script>
<div class="container mx-auto px-4 py-8">
<h1>Payment Successful!</h1>
{#if checkout_id}
<p>Checkout ID: {checkout_id}</p>
{/if}
</div>