mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat: add clerk auth support with convex (#548)
This commit is contained in:
@@ -77,7 +77,7 @@ Database models are located in `apps/server/src/db/models/`
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
## Authentication
|
||||
|
||||
Authentication is enabled in this project:
|
||||
@@ -129,4 +129,4 @@ This project includes a `bts.jsonc` configuration file that stores your Better-T
|
||||
- Turborepo handles build caching and parallel execution
|
||||
{{/if}}
|
||||
- Use `{{#if (eq packageManager "bun")}}bunx{{else if (eq packageManager "pnpm")}}pnpx{{else}}npx{{/if}}
|
||||
create-better-t-stack add` to add more features later
|
||||
create-better-t-stack add` to add more features later
|
||||
|
||||
@@ -3,7 +3,7 @@ import { RPCLink } from "@orpc/client/fetch";
|
||||
import { createTanstackQueryUtils } from "@orpc/tanstack-query";
|
||||
import { QueryCache, QueryClient } from "@tanstack/react-query";
|
||||
import type { AppRouterClient } from "../../server/src/routers";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
{{/if}}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const queryClient = new QueryClient({
|
||||
|
||||
export const link = new RPCLink({
|
||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/rpc`,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
headers() {
|
||||
const headers = new Map<string, string>();
|
||||
const cookies = authClient.getCookie();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{#if (eq backend 'next')}}
|
||||
import type { NextRequest } from "next/server";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext(req: NextRequest) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
@@ -19,7 +19,7 @@ export async function createContext(req: NextRequest) {
|
||||
|
||||
{{else if (eq backend 'hono')}}
|
||||
import type { Context as HonoContext } from "hono";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -28,7 +28,7 @@ export type CreateContextOptions = {
|
||||
};
|
||||
|
||||
export async function createContext({ context }: CreateContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.req.raw.headers,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ export async function createContext({ context }: CreateContextOptions) {
|
||||
|
||||
{{else if (eq backend 'elysia')}}
|
||||
import type { Context as ElysiaContext } from "elysia";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -54,7 +54,7 @@ export type CreateContextOptions = {
|
||||
};
|
||||
|
||||
export async function createContext({ context }: CreateContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.request.headers,
|
||||
});
|
||||
@@ -70,13 +70,13 @@ export async function createContext({ context }: CreateContextOptions) {
|
||||
}
|
||||
|
||||
{{else if (eq backend 'express')}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext(opts: any) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(opts.req.headers),
|
||||
});
|
||||
@@ -93,13 +93,13 @@ export async function createContext(opts: any) {
|
||||
|
||||
{{else if (eq backend 'fastify')}}
|
||||
import type { IncomingHttpHeaders } from "node:http";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext(req: IncomingHttpHeaders) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req),
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ export const o = os.$context<Context>();
|
||||
|
||||
export const publicProcedure = o;
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const requireAuth = o.middleware(async ({ context, next }) => {
|
||||
if (!context.session?.user) {
|
||||
throw new ORPCError("UNAUTHORIZED");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { createContext } from '@/lib/context'
|
||||
{{/if}}
|
||||
import { appRouter } from '@/routers'
|
||||
@@ -10,7 +10,7 @@ const handler = new RPCHandler(appRouter)
|
||||
async function handleRequest(req: NextRequest) {
|
||||
const { response } = await handler.handle(req, {
|
||||
prefix: '/rpc',
|
||||
context: {{#if auth}}await createContext(req){{else}}{}{{/if}},
|
||||
context: {{#if (eq auth "better-auth")}}await createContext(req){{else}}{}{{/if}},
|
||||
})
|
||||
|
||||
return response ?? new Response('Not found', { status: 404 })
|
||||
|
||||
@@ -12,7 +12,7 @@ export default defineNuxtPlugin(() => {
|
||||
|
||||
const rpcLink = new RPCLink({
|
||||
url: rpcUrl,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
|
||||
@@ -26,7 +26,7 @@ export const link = new RPCLink({
|
||||
{{else}}
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
|
||||
@@ -14,7 +14,7 @@ export const queryClient = new QueryClient({
|
||||
|
||||
export const link = new RPCLink({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/rpc`,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
|
||||
@@ -15,7 +15,7 @@ export const queryClient = new QueryClient({
|
||||
|
||||
export const link = new RPCLink({
|
||||
url: `${PUBLIC_SERVER_URL}/rpc`,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
{{/if}}
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
@@ -12,7 +12,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${process.env.EXPO_PUBLIC_SERVER_URL}/trpc`,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
headers() {
|
||||
const headers = new Map<string, string>();
|
||||
const cookies = authClient.getCookie();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{#if (eq backend 'next')}}
|
||||
import type { NextRequest } from "next/server";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext(req: NextRequest) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: req.headers,
|
||||
});
|
||||
@@ -22,7 +22,7 @@ export async function createContext(req: NextRequest) {
|
||||
|
||||
{{else if (eq backend 'hono')}}
|
||||
import type { Context as HonoContext } from "hono";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -31,7 +31,7 @@ export type CreateContextOptions = {
|
||||
};
|
||||
|
||||
export async function createContext({ context }: CreateContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.req.raw.headers,
|
||||
});
|
||||
@@ -48,7 +48,7 @@ export async function createContext({ context }: CreateContextOptions) {
|
||||
|
||||
{{else if (eq backend 'elysia')}}
|
||||
import type { Context as ElysiaContext } from "elysia";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -57,7 +57,7 @@ export type CreateContextOptions = {
|
||||
};
|
||||
|
||||
export async function createContext({ context }: CreateContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: context.request.headers,
|
||||
});
|
||||
@@ -74,13 +74,13 @@ export async function createContext({ context }: CreateContextOptions) {
|
||||
|
||||
{{else if (eq backend 'express')}}
|
||||
import type { CreateExpressContextOptions } from "@trpc/server/adapters/express";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext(opts: CreateExpressContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(opts.req.headers),
|
||||
});
|
||||
@@ -97,13 +97,13 @@ export async function createContext(opts: CreateExpressContextOptions) {
|
||||
|
||||
{{else if (eq backend 'fastify')}}
|
||||
import type { CreateFastifyContextOptions } from "@trpc/server/adapters/fastify";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { fromNodeHeaders } from "better-auth/node";
|
||||
import { auth } from "./auth";
|
||||
{{/if}}
|
||||
|
||||
export async function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
const session = await auth.api.getSession({
|
||||
headers: fromNodeHeaders(req.headers),
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export const router = t.router;
|
||||
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
|
||||
if (!ctx.session) {
|
||||
throw new TRPCError({
|
||||
|
||||
@@ -28,7 +28,7 @@ const trpcClient = createTRPCClient<AppRouter>({
|
||||
{{else}}
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
@@ -78,7 +78,7 @@ export const trpcClient = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fetch(url, options) {
|
||||
return fetch(url, {
|
||||
...options,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
// Replace with your own Clerk Issuer URL from your "convex" JWT template
|
||||
// or with `process.env.CLERK_JWT_ISSUER_DOMAIN`
|
||||
// and configure CLERK_JWT_ISSUER_DOMAIN on the Convex Dashboard
|
||||
// See https://docs.convex.dev/auth/clerk#configuring-dev-and-prod-instances
|
||||
domain: process.env.CLERK_JWT_ISSUER_DOMAIN,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { query } from "./_generated/server";
|
||||
|
||||
export const get = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
if (identity === null) {
|
||||
return {
|
||||
message: "Not authenticated",
|
||||
};
|
||||
}
|
||||
return {
|
||||
message: "This is private",
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Redirect, Stack } from "expo-router";
|
||||
import { useAuth } from "@clerk/clerk-expo";
|
||||
|
||||
export default function AuthRoutesLayout() {
|
||||
const { isSignedIn } = useAuth();
|
||||
|
||||
if (isSignedIn) {
|
||||
return <Redirect href={"/"} />;
|
||||
}
|
||||
|
||||
return <Stack />;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useSignIn } from "@clerk/clerk-expo";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import React from "react";
|
||||
|
||||
export default function Page() {
|
||||
const { signIn, setActive, isLoaded } = useSignIn();
|
||||
const router = useRouter();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
|
||||
// Handle the submission of the sign-in form
|
||||
const onSignInPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
// Start the sign-in process using the email and password provided
|
||||
try {
|
||||
const signInAttempt = await signIn.create({
|
||||
identifier: emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
// If sign-in process is complete, set the created session as active
|
||||
// and redirect the user
|
||||
if (signInAttempt.status === "complete") {
|
||||
await setActive({ session: signInAttempt.createdSessionId });
|
||||
router.replace("/");
|
||||
} else {
|
||||
// If the status isn't complete, check why. User might need to
|
||||
// complete further steps.
|
||||
console.error(JSON.stringify(signInAttempt, null, 2));
|
||||
}
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Sign in</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter email"
|
||||
onChangeText={(emailAddress) => setEmailAddress(emailAddress)}
|
||||
/>
|
||||
<TextInput
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
/>
|
||||
<TouchableOpacity onPress={onSignInPress}>
|
||||
<Text>Continue</Text>
|
||||
</TouchableOpacity>
|
||||
<View style=\{{ display: "flex", flexDirection: "row", gap: 3 }}>
|
||||
<Text>Don't have an account?</Text>
|
||||
<Link href="/sign-up">
|
||||
<Text>Sign up</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import * as React from "react";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { useSignUp } from "@clerk/clerk-expo";
|
||||
import { Link, useRouter } from "expo-router";
|
||||
|
||||
export default function SignUpScreen() {
|
||||
const { isLoaded, signUp, setActive } = useSignUp();
|
||||
const router = useRouter();
|
||||
|
||||
const [emailAddress, setEmailAddress] = React.useState("");
|
||||
const [password, setPassword] = React.useState("");
|
||||
const [pendingVerification, setPendingVerification] = React.useState(false);
|
||||
const [code, setCode] = React.useState("");
|
||||
|
||||
// Handle submission of sign-up form
|
||||
const onSignUpPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
console.log(emailAddress, password);
|
||||
|
||||
// Start sign-up process using email and password provided
|
||||
try {
|
||||
await signUp.create({
|
||||
emailAddress,
|
||||
password,
|
||||
});
|
||||
|
||||
// Send user an email with verification code
|
||||
await signUp.prepareEmailAddressVerification({ strategy: "email_code" });
|
||||
|
||||
// Set 'pendingVerification' to true to display second form
|
||||
// and capture OTP code
|
||||
setPendingVerification(true);
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
// Handle submission of verification form
|
||||
const onVerifyPress = async () => {
|
||||
if (!isLoaded) return;
|
||||
|
||||
try {
|
||||
// Use the code the user provided to attempt verification
|
||||
const signUpAttempt = await signUp.attemptEmailAddressVerification({
|
||||
code,
|
||||
});
|
||||
|
||||
// If verification was completed, set the session to active
|
||||
// and redirect the user
|
||||
if (signUpAttempt.status === "complete") {
|
||||
await setActive({ session: signUpAttempt.createdSessionId });
|
||||
router.replace("/");
|
||||
} else {
|
||||
// If the status is not complete, check why. User may need to
|
||||
// complete further steps.
|
||||
console.error(JSON.stringify(signUpAttempt, null, 2));
|
||||
}
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
if (pendingVerification) {
|
||||
return (
|
||||
<>
|
||||
<Text>Verify your email</Text>
|
||||
<TextInput
|
||||
value={code}
|
||||
placeholder="Enter your verification code"
|
||||
onChangeText={(code) => setCode(code)}
|
||||
/>
|
||||
<TouchableOpacity onPress={onVerifyPress}>
|
||||
<Text>Verify</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text>Sign up</Text>
|
||||
<TextInput
|
||||
autoCapitalize="none"
|
||||
value={emailAddress}
|
||||
placeholder="Enter email"
|
||||
onChangeText={(email) => setEmailAddress(email)}
|
||||
/>
|
||||
<TextInput
|
||||
value={password}
|
||||
placeholder="Enter password"
|
||||
secureTextEntry={true}
|
||||
onChangeText={(password) => setPassword(password)}
|
||||
/>
|
||||
<TouchableOpacity onPress={onSignUpPress}>
|
||||
<Text>Continue</Text>
|
||||
</TouchableOpacity>
|
||||
<View style=\{{ display: "flex", flexDirection: "row", gap: 3 }}>
|
||||
<Text>Already have an account?</Text>
|
||||
<Link href="/sign-in">
|
||||
<Text>Sign in</Text>
|
||||
</Link>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useClerk } from "@clerk/clerk-expo";
|
||||
import { useRouter } from "expo-router";
|
||||
import { Text, TouchableOpacity } from "react-native";
|
||||
|
||||
export const SignOutButton = () => {
|
||||
// Use `useClerk()` to access the `signOut()` function
|
||||
const { signOut } = useClerk();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await signOut();
|
||||
// Redirect to your desired page
|
||||
router.replace("/");
|
||||
} catch (err) {
|
||||
// See https://clerk.com/docs/custom-flows/error-handling
|
||||
// for more info on error handling
|
||||
console.error(JSON.stringify(err, null, 2));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handleSignOut}>
|
||||
<Text>Sign out</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||
import { SignInButton, UserButton, useUser } from "@clerk/nextjs";
|
||||
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
|
||||
|
||||
export default function Dashboard() {
|
||||
const user = useUser();
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Authenticated>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome {user.user?.fullName}</p>
|
||||
<p>privateData: {privateData?.message}</p>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<SignInButton />
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<div>Loading...</div>
|
||||
</AuthLoading>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { clerkMiddleware } from "@clerk/nextjs/server";
|
||||
|
||||
export default clerkMiddleware();
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Skip Next.js internals and all static files, unless found in search params
|
||||
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
||||
// Always run for API routes
|
||||
"/(api|trpc)(.*)",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import { SignInButton, UserButton, useUser } from "@clerk/clerk-react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||
import {
|
||||
Authenticated,
|
||||
AuthLoading,
|
||||
Unauthenticated,
|
||||
useQuery,
|
||||
} from "convex/react";
|
||||
|
||||
export default function Dashboard() {
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Authenticated>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome {user.user?.fullName}</p>
|
||||
<p>privateData: {privateData?.message}</p>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<SignInButton />
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<div>Loading...</div>
|
||||
</AuthLoading>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SignInButton, UserButton, useUser } from "@clerk/clerk-react";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
Authenticated,
|
||||
AuthLoading,
|
||||
Unauthenticated,
|
||||
useQuery,
|
||||
} from "convex/react";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Authenticated>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome {user.user?.fullName}</p>
|
||||
<p>privateData: {privateData?.message}</p>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<SignInButton />
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<div>Loading...</div>
|
||||
</AuthLoading>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { SignInButton, UserButton, useUser } from "@clerk/tanstack-react-start";
|
||||
import { api } from "@{{projectName}}/backend/convex/_generated/api";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import {
|
||||
Authenticated,
|
||||
AuthLoading,
|
||||
Unauthenticated,
|
||||
useQuery,
|
||||
} from "convex/react";
|
||||
|
||||
export const Route = createFileRoute("/dashboard")({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
const user = useUser();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Authenticated>
|
||||
<div>
|
||||
<h1>Dashboard</h1>
|
||||
<p>Welcome {user.user?.fullName}</p>
|
||||
<p>privateData: {privateData?.message}</p>
|
||||
<UserButton />
|
||||
</div>
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<SignInButton />
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<div>Loading...</div>
|
||||
</AuthLoading>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { createClerkHandler } from "@clerk/tanstack-react-start/server";
|
||||
import {
|
||||
createStartHandler,
|
||||
defaultStreamHandler,
|
||||
defineHandlerCallback,
|
||||
} from "@tanstack/react-start/server";
|
||||
import { createRouter } from "./router";
|
||||
|
||||
const handlerFactory = createClerkHandler(
|
||||
createStartHandler({
|
||||
createRouter,
|
||||
}),
|
||||
);
|
||||
|
||||
export default defineHandlerCallback(async (event) => {
|
||||
const startHandler = await handlerFactory(defaultStreamHandler);
|
||||
return startHandler(event);
|
||||
});
|
||||
@@ -9,6 +9,7 @@
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.0",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { RPCHandler } from "@orpc/server/fetch";
|
||||
import { appRouter } from "./routers";
|
||||
import { createContext } from "./lib/context";
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./lib/auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -31,13 +31,13 @@ const app = new Elysia()
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN || "",
|
||||
methods: ["GET", "POST", "OPTIONS"],
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
credentials: true,
|
||||
{{/if}}
|
||||
}),
|
||||
)
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
.all("/api/auth/*", async (context) => {
|
||||
const { request } = context;
|
||||
if (["POST", "GET"].includes(request.method)) {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { appRouter } from "./routers/index";
|
||||
{{#if (eq api "orpc")}}
|
||||
import { RPCHandler } from "@orpc/server/node";
|
||||
import { appRouter } from "./routers";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { createContext } from "./lib/context";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@@ -17,7 +17,7 @@ import express from "express";
|
||||
import { streamText, type UIMessage, convertToModelMessages } from "ai";
|
||||
import { google } from "@ai-sdk/google";
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./lib/auth";
|
||||
import { toNodeHandler } from "better-auth/node";
|
||||
{{/if}}
|
||||
@@ -28,14 +28,14 @@ app.use(
|
||||
cors({
|
||||
origin: process.env.CORS_ORIGIN || "",
|
||||
methods: ["GET", "POST", "OPTIONS"],
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
allowedHeaders: ["Content-Type", "Authorization"],
|
||||
credentials: true,
|
||||
{{/if}}
|
||||
})
|
||||
);
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
app.all("/api/auth{/*path}", toNodeHandler(auth));
|
||||
{{/if}}
|
||||
|
||||
@@ -54,7 +54,7 @@ const handler = new RPCHandler(appRouter);
|
||||
app.use("/rpc{*path}", async (req, res, next) => {
|
||||
const { matched } = await handler.handle(req, res, {
|
||||
prefix: "/rpc",
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
context: await createContext({ req }),
|
||||
{{else}}
|
||||
context: {},
|
||||
@@ -85,4 +85,4 @@ app.get("/", (_req, res) => {
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on port ${port}`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { RPCHandler } from "@orpc/server/node";
|
||||
import { CORSPlugin } from "@orpc/server/plugins";
|
||||
import { appRouter } from "./routers/index";
|
||||
import { createServer } from "node:http";
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { createContext } from "./lib/context";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@@ -23,7 +23,7 @@ import { streamText, type UIMessage, convertToModelMessages } from "ai";
|
||||
import { google } from "@ai-sdk/google";
|
||||
{{/if}}
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./lib/auth";
|
||||
{{/if}}
|
||||
|
||||
@@ -77,7 +77,7 @@ const fastify = Fastify({
|
||||
|
||||
fastify.register(fastifyCors, baseCorsConfig);
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
fastify.route({
|
||||
method: ["GET", "POST"],
|
||||
url: "/api/auth/*",
|
||||
@@ -149,4 +149,4 @@ fastify.listen({ port: 3000 }, (err) => {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Server running on port 3000");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ import { trpcServer } from "@hono/trpc-server";
|
||||
import { createContext } from "./lib/context";
|
||||
import { appRouter } from "./routers/index";
|
||||
{{/if}}
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import { auth } from "./lib/auth";
|
||||
{{/if}}
|
||||
import { Hono } from "hono";
|
||||
@@ -42,14 +42,14 @@ app.use(
|
||||
origin: env.CORS_ORIGIN || "",
|
||||
{{/if}}
|
||||
allowMethods: ["GET", "POST", "OPTIONS"],
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
allowHeaders: ["Content-Type", "Authorization"],
|
||||
credentials: true,
|
||||
{{/if}}
|
||||
})
|
||||
);
|
||||
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
|
||||
{{/if}}
|
||||
|
||||
@@ -133,4 +133,4 @@ export default app;
|
||||
{{#if (eq runtime "workers")}}
|
||||
export default app;
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{#if (eq api "orpc")}}
|
||||
import { {{#if auth}}protectedProcedure, {{/if}}publicProcedure } from "../lib/orpc";
|
||||
import { {{#if (eq auth "better-auth")}}protectedProcedure, {{/if}}publicProcedure } from "../lib/orpc";
|
||||
import type { RouterClient } from "@orpc/server";
|
||||
{{#if (includes examples "todo")}}
|
||||
import { todoRouter } from "./todo";
|
||||
@@ -9,7 +9,7 @@ export const appRouter = {
|
||||
healthCheck: publicProcedure.handler(() => {
|
||||
return "OK";
|
||||
}),
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
privateData: protectedProcedure.handler(({ context }) => {
|
||||
return {
|
||||
message: "This is private",
|
||||
@@ -25,7 +25,7 @@ export type AppRouter = typeof appRouter;
|
||||
export type AppRouterClient = RouterClient<typeof appRouter>;
|
||||
{{else if (eq api "trpc")}}
|
||||
import {
|
||||
{{#if auth}}protectedProcedure, {{/if}}publicProcedure,
|
||||
{{#if (eq auth "better-auth")}}protectedProcedure, {{/if}}publicProcedure,
|
||||
router,
|
||||
} from "../lib/trpc";
|
||||
{{#if (includes examples "todo")}}
|
||||
@@ -36,7 +36,7 @@ export const appRouter = router({
|
||||
healthCheck: publicProcedure.query(() => {
|
||||
return "OK";
|
||||
}),
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
privateData: protectedProcedure.query(({ ctx }) => {
|
||||
return {
|
||||
message: "This is private",
|
||||
|
||||
@@ -164,7 +164,7 @@ export const server = await Worker("server", {
|
||||
DATABASE_URL: alchemy.secret(process.env.DATABASE_URL),
|
||||
{{/if}}
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN || "",
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
BETTER_AUTH_SECRET: alchemy.secret(process.env.BETTER_AUTH_SECRET),
|
||||
BETTER_AUTH_URL: process.env.BETTER_AUTH_URL || "",
|
||||
{{/if}}
|
||||
@@ -190,4 +190,4 @@ console.log(`Web -> ${web.url}`);
|
||||
console.log(`Server -> ${server.url}`);
|
||||
{{/if}}
|
||||
|
||||
await app.finalize();
|
||||
await app.finalize();
|
||||
|
||||
@@ -9,9 +9,17 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { Link } from "expo-router";
|
||||
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { SignOutButton } from "@/components/sign-out-button";
|
||||
{{else}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
export default function Home() {
|
||||
{{#if (eq api "orpc")}}
|
||||
@@ -21,21 +29,27 @@ export default function Home() {
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
const { user } = useUser();
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
{{else}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ScrollView showsVerticalScrollIndicator={false} className="flex-1">
|
||||
<Text className="font-mono text-foreground text-3xl font-bold mb-4">
|
||||
BETTER T STACK
|
||||
</Text>
|
||||
BETTER T STACK
|
||||
</Text>
|
||||
<View className="bg-card border border-border rounded-xl p-6 mb-6 shadow-sm">
|
||||
{{#if (eq backend "convex")}}
|
||||
<View className="flex-row items-center gap-3">
|
||||
<View
|
||||
className={`h-3 w-3 rounded-full ${
|
||||
healthCheck ? "bg-green-500" : "bg-orange-500"
|
||||
healthCheck ? "bg-green-500" : "bg-orange-500"
|
||||
}`}
|
||||
/>
|
||||
<View className="flex-1">
|
||||
@@ -89,6 +103,24 @@ export default function Home() {
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</View>
|
||||
{{#if (and (eq backend "convex") (eq auth "clerk"))}}
|
||||
<Authenticated>
|
||||
<Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
|
||||
<Text>Private Data: {privateData?.message}</Text>
|
||||
<SignOutButton />
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<Link href="/(auth)/sign-in">
|
||||
<Text>Sign in</Text>
|
||||
</Link>
|
||||
<Link href="/(auth)/sign-up">
|
||||
<Text>Sign up</Text>
|
||||
</Link>
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<Text>Loading...</Text>
|
||||
</AuthLoading>
|
||||
{{/if}}
|
||||
</ScrollView>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,11 @@ import "@/polyfills";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { ClerkProvider, useAuth } from "@clerk/clerk-expo";
|
||||
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
||||
import { tokenCache } from "@clerk/clerk-expo/token-cache";
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
@@ -72,6 +77,28 @@ export default function RootLayout() {
|
||||
}
|
||||
return (
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
<ClerkProvider
|
||||
tokenCache={tokenCache}
|
||||
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||
>
|
||||
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
||||
<GestureHandlerRootView style=\{{ flex: 1 }}>
|
||||
<Stack>
|
||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options=\{{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options=\{{ title: "Modal", presentation: "modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</ThemeProvider>
|
||||
</ConvexProviderWithClerk>
|
||||
</ClerkProvider>
|
||||
{{else}}
|
||||
<ConvexProvider client={convex}>
|
||||
<ThemeProvider value={isDarkColorScheme ? DARK_THEME : LIGHT_THEME}>
|
||||
<StatusBar style={isDarkColorScheme ? "light" : "dark"} />
|
||||
@@ -86,6 +113,7 @@ export default function RootLayout() {
|
||||
</GestureHandlerRootView>
|
||||
</ThemeProvider>
|
||||
</ConvexProvider>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
{{/if}}
|
||||
"expo": "^53.0.4",
|
||||
"expo-constants": "~17.1.4",
|
||||
"expo-crypto": "~14.1.5",
|
||||
"expo-linking": "~7.1.4",
|
||||
"expo-navigation-bar": "~4.2.3",
|
||||
"expo-router": "~5.0.3",
|
||||
|
||||
@@ -11,9 +11,17 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { trpc } from "@/utils/trpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { Link } from "expo-router";
|
||||
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
||||
import { useUser } from "@clerk/clerk-expo";
|
||||
import { SignOutButton } from "@/components/sign-out-button";
|
||||
{{else}}
|
||||
import { useQuery } from "convex/react";
|
||||
import { api } from "@{{ projectName }}/backend/convex/_generated/api";
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
export default function Home() {
|
||||
{{#if (eq api "orpc")}}
|
||||
@@ -23,7 +31,13 @@ export default function Home() {
|
||||
const healthCheck = useQuery(trpc.healthCheck.queryOptions());
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
const { user } = useUser();
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
const privateData = useQuery(api.privateData.get);
|
||||
{{else}}
|
||||
const healthCheck = useQuery(api.healthCheck.get);
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
return (
|
||||
@@ -105,6 +119,24 @@ export default function Home() {
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</View>
|
||||
{{#if (and (eq backend "convex") (eq auth "clerk"))}}
|
||||
<Authenticated>
|
||||
<Text>Hello {user?.emailAddresses[0].emailAddress}</Text>
|
||||
<Text>Private Data: {privateData?.message}</Text>
|
||||
<SignOutButton />
|
||||
</Authenticated>
|
||||
<Unauthenticated>
|
||||
<Link href="/(auth)/sign-in">
|
||||
<Text>Sign in</Text>
|
||||
</Link>
|
||||
<Link href="/(auth)/sign-up">
|
||||
<Text>Sign up</Text>
|
||||
</Link>
|
||||
</Unauthenticated>
|
||||
<AuthLoading>
|
||||
<Text>Loading...</Text>
|
||||
</AuthLoading>
|
||||
{{/if}}
|
||||
</ScrollView>
|
||||
</Container>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,11 @@ import { queryClient } from "@/utils/orpc";
|
||||
{{/if}}
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { ClerkProvider, useAuth } from "@clerk/clerk-expo";
|
||||
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
||||
import { tokenCache } from "@clerk/clerk-expo/token-cache";
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
@@ -34,6 +39,35 @@ export default function RootLayout() {
|
||||
|
||||
return (
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
<ClerkProvider
|
||||
tokenCache={tokenCache}
|
||||
publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}
|
||||
>
|
||||
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
||||
<GestureHandlerRootView style=\{{ flex: 1 }}>
|
||||
<Stack
|
||||
screenOptions=\{{
|
||||
headerStyle: {
|
||||
backgroundColor: theme.colors.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
headerTintColor: theme.colors.foreground,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen name="(drawer)" options=\{{ headerShown: false }} />
|
||||
<Stack.Screen name="(auth)" options=\{{ headerShown: false }} />
|
||||
<Stack.Screen
|
||||
name="modal"
|
||||
options=\{{ title: "Modal", presentation: "modal" }}
|
||||
/>
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</ConvexProviderWithClerk>
|
||||
</ClerkProvider>
|
||||
{{else}}
|
||||
<ConvexProvider client={convex}>
|
||||
<GestureHandlerRootView style=\{{ flex: 1 }}>
|
||||
<Stack
|
||||
@@ -55,6 +89,7 @@ export default function RootLayout() {
|
||||
</Stack>
|
||||
</GestureHandlerRootView>
|
||||
</ConvexProvider>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.2",
|
||||
"expo": "^53.0.17",
|
||||
"expo-constants": "~17.1.7",
|
||||
"expo-crypto": "~14.1.5",
|
||||
"expo-linking": "~7.1.7",
|
||||
"expo-router": "~5.1.3",
|
||||
"expo-secure-store": "~14.2.3",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import ModeToggle from './ModeToggle.vue'
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
import UserMenu from './UserMenu.vue'
|
||||
{{/if}}
|
||||
|
||||
const links = [
|
||||
{ to: "/", label: "Home" },
|
||||
{{#if auth}}
|
||||
{{#if (or (eq auth "better-auth") (eq auth "clerk"))}}
|
||||
{ to: "/dashboard", label: "Dashboard" },
|
||||
{{/if}}
|
||||
{{#if (includes examples "todo")}}
|
||||
@@ -34,7 +34,7 @@ const links = [
|
||||
</nav>
|
||||
<div class="flex items-center gap-2">
|
||||
<ModeToggle />
|
||||
{{#if auth}}
|
||||
{{#if (eq auth "better-auth")}}
|
||||
<UserMenu />
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "../index.css";
|
||||
import Providers from "@/components/providers";
|
||||
{{#if (eq auth "clerk")}}{{#if (eq backend "convex")}}import { ClerkProvider } from "@clerk/nextjs";
|
||||
{{/if}}{{/if}}import Providers from "@/components/providers";
|
||||
import Header from "@/components/header";
|
||||
|
||||
const geistSans = Geist({
|
||||
@@ -24,18 +25,25 @@ export default function RootLayout({
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Providers>
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{{#if (and (eq auth "clerk") (eq backend "convex"))}}<ClerkProvider>
|
||||
<Providers>
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
</Providers>
|
||||
</ClerkProvider>{{else}}<Providers>
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
{children}
|
||||
</div>
|
||||
</Providers>{{/if}}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
"use client";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { useAuth } from "@clerk/nextjs";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
||||
{{else}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
@@ -34,7 +40,13 @@ export default function Providers({
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{{#if (eq backend "convex")}}
|
||||
{{#if (eq auth "clerk")}}
|
||||
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
||||
{children}
|
||||
</ConvexProviderWithClerk>
|
||||
{{else}}
|
||||
<ConvexProvider client={convex}>{children}</ConvexProvider>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -13,7 +13,13 @@ import { ThemeProvider } from "./components/theme-provider";
|
||||
import { Toaster } from "./components/ui/sonner";
|
||||
|
||||
{{#if (eq backend "convex")}}
|
||||
import { ConvexProvider, ConvexReactClient } from "convex/react";
|
||||
import { ConvexReactClient } from "convex/react";
|
||||
{{#if (eq auth "clerk")}}
|
||||
import { ClerkProvider, useAuth } from "@clerk/clerk-react";
|
||||
import { ConvexProviderWithClerk } from "convex/react-clerk";
|
||||
{{else}}
|
||||
import { ConvexProvider } from "convex/react";
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#unless (eq api "none")}}
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
@@ -60,6 +66,26 @@ export default function App() {
|
||||
const convex = new ConvexReactClient(
|
||||
import.meta.env.VITE_CONVEX_URL as string,
|
||||
);
|
||||
{{#if (eq auth "clerk")}}
|
||||
return (
|
||||
<ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
|
||||
<ConvexProviderWithClerk client={convex} useAuth={useAuth}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="dark"
|
||||
disableTransitionOnChange
|
||||
storageKey="vite-ui-theme"
|
||||
>
|
||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||
<Header />
|
||||
<Outlet />
|
||||
</div>
|
||||
<Toaster richColors />
|
||||
</ThemeProvider>
|
||||
</ConvexProviderWithClerk>
|
||||
</ClerkProvider>
|
||||
);
|
||||
{{else}}
|
||||
return (
|
||||
<ConvexProvider client={convex}>
|
||||
<ThemeProvider
|
||||
@@ -76,6 +102,7 @@ export default function App() {
|
||||
</ThemeProvider>
|
||||
</ConvexProvider>
|
||||
);
|
||||
{{/if}}
|
||||
}
|
||||
{{else if (eq api "orpc")}}
|
||||
export default function App() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user