diff --git a/.cursor/rules/better-t-stack-repo.mdc b/.cursor/rules/better-t-stack-repo.mdc index 4141758db..17fef7df7 100644 --- a/.cursor/rules/better-t-stack-repo.mdc +++ b/.cursor/rules/better-t-stack-repo.mdc @@ -1,10 +1,10 @@ --- alwaysApply: true --- - - Always use functional programming; avoid object-oriented programming. - Define functions using the standard function declaration syntax, not arrow functions. - Do not include emojis. - Use TypeScript type aliases instead of interface declarations. - In Handlebars templates, avoid generic if/else blocks. Write explicit conditions, such as: use if (eq orm "prisma") for Prisma, and else if (eq orm "drizzle") for Drizzle. -- Do not use explicit return types \ No newline at end of file +- Do not use explicit return types +- escape the '{{' in hbs templates like '\{{' \ No newline at end of file diff --git a/apps/cli/README.md b/apps/cli/README.md index 6001e742e..7d456676f 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -57,7 +57,7 @@ Options: --orm ORM type (none, drizzle, prisma, mongoose) --auth Include authentication --no-auth Exclude authentication - --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none) + --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-bare, native-uniwind, native-unistyles, none) --addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, fumadocs, ultracite, oxlint, none) --examples Examples to include (todo, ai, none) --git Initialize git repository @@ -119,7 +119,7 @@ npx create-better-t-stack my-app --backend elysia --runtime node Create a project with multiple frontend options (one web + one native): ```bash -npx create-better-t-stack my-app --frontend tanstack-router native-nativewind +npx create-better-t-stack my-app --frontend tanstack-router native-bare ``` Create a project with examples: diff --git a/apps/cli/src/constants.ts b/apps/cli/src/constants.ts index 86dda0eab..8ef86bb4c 100644 --- a/apps/cli/src/constants.ts +++ b/apps/cli/src/constants.ts @@ -155,7 +155,7 @@ export const dependencyVersionMap = { "@sveltejs/adapter-cloudflare": "^7.2.1", "@cloudflare/workers-types": "^4.20250822.0", - alchemy: "^0.75.1", + alchemy: "^0.77.0", dotenv: "^17.2.2", tsdown: "^0.15.5", diff --git a/apps/cli/src/helpers/addons/examples-setup.ts b/apps/cli/src/helpers/addons/examples-setup.ts index 9f1ec54ef..3b80e920e 100644 --- a/apps/cli/src/helpers/addons/examples-setup.ts +++ b/apps/cli/src/helpers/addons/examples-setup.ts @@ -56,7 +56,8 @@ export async function setupExamples(config: ProjectConfig) { frontend.includes("tanstack-start"); const hasNext = frontend.includes("next"); const hasReactNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); if (webClientDirExists) { diff --git a/apps/cli/src/helpers/addons/ultracite-setup.ts b/apps/cli/src/helpers/addons/ultracite-setup.ts index 5391ed62b..717f2ae6b 100644 --- a/apps/cli/src/helpers/addons/ultracite-setup.ts +++ b/apps/cli/src/helpers/addons/ultracite-setup.ts @@ -118,7 +118,8 @@ function getFrameworksFromFrontend(frontend: string[]): string[] { "tanstack-start": "react", next: "next", nuxt: "vue", - "native-nativewind": "react", + "native-bare": "react", + "native-uniwind": "react", "native-unistyles": "react", svelte: "svelte", solid: "solid", diff --git a/apps/cli/src/helpers/core/api-setup.ts b/apps/cli/src/helpers/core/api-setup.ts index 02a09c539..efed7f8b7 100644 --- a/apps/cli/src/helpers/core/api-setup.ts +++ b/apps/cli/src/helpers/core/api-setup.ts @@ -33,7 +33,7 @@ function getFrontendType(frontend: Frontend[]): { "tanstack-start", "next", ]; - const nativeFrontends = ["native-nativewind", "native-unistyles"]; + const nativeFrontends = ["native-bare", "native-uniwind", "native-unistyles"]; return { hasReactWeb: frontend.some((f) => reactBasedFrontends.includes(f)), @@ -149,7 +149,8 @@ function getQueryDependencies(frontend: Frontend[]) { "tanstack-router", "tanstack-start", "next", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; @@ -162,12 +163,14 @@ function getQueryDependencies(frontend: Frontend[]) { if (needsReactQuery) { const hasReactWeb = frontend.some( (f) => - f !== "native-nativewind" && + f !== "native-bare" && + f !== "native-uniwind" && f !== "native-unistyles" && reactBasedFrontends.includes(f), ); const hasNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); if (hasReactWeb) { diff --git a/apps/cli/src/helpers/core/auth-setup.ts b/apps/cli/src/helpers/core/auth-setup.ts index 5dfdc6a64..bf1c543aa 100644 --- a/apps/cli/src/helpers/core/auth-setup.ts +++ b/apps/cli/src/helpers/core/auth-setup.ts @@ -52,7 +52,8 @@ export async function setupAuth(config: ProjectConfig) { const convexBackendDirExists = await fs.pathExists(convexBackendDir); const hasNativeForBA = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); if (convexBackendDirExists) { @@ -98,9 +99,13 @@ export async function setupAuth(config: ProjectConfig) { } } - const hasNativeWind = frontend.includes("native-nativewind"); + const hasNativeBare = frontend.includes("native-bare"); + const hasNativeUniwind = frontend.includes("native-uniwind"); const hasUnistyles = frontend.includes("native-unistyles"); - if (nativeDirExists && (hasNativeWind || hasUnistyles)) { + if ( + nativeDirExists && + (hasNativeBare || hasNativeUniwind || hasUnistyles) + ) { await addPackageDependency({ dependencies: [ "better-auth", @@ -116,12 +121,13 @@ export async function setupAuth(config: ProjectConfig) { } } - const hasNativeWind = frontend.includes("native-nativewind"); + const hasNativeBare = frontend.includes("native-bare"); + const hasNativeUniwind = frontend.includes("native-uniwind"); const hasUnistyles = frontend.includes("native-unistyles"); if ( auth === "clerk" && nativeDirExists && - (hasNativeWind || hasUnistyles) + (hasNativeBare || hasNativeUniwind || hasUnistyles) ) { await addPackageDependency({ dependencies: ["@clerk/clerk-expo"], @@ -163,7 +169,8 @@ export async function setupAuth(config: ProjectConfig) { } if ( - (frontend.includes("native-nativewind") || + (frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles")) && nativeDirExists ) { diff --git a/apps/cli/src/helpers/core/create-readme.ts b/apps/cli/src/helpers/core/create-readme.ts index 83c6aa506..ab91791f9 100644 --- a/apps/cli/src/helpers/core/create-readme.ts +++ b/apps/cli/src/helpers/core/create-readme.ts @@ -43,7 +43,8 @@ function generateReadmeContent(options: ProjectConfig) { const isConvex = backend === "convex"; const hasReactRouter = frontend.includes("react-router"); const hasNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); const hasSvelte = frontend.includes("svelte"); @@ -321,7 +322,8 @@ function generateProjectStructure( } const hasNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); if (hasNative) { if (isBackendSelf) { @@ -402,7 +404,8 @@ function generateFeaturesList( const hasTanstackRouter = frontend.includes("tanstack-router"); const hasReactRouter = frontend.includes("react-router"); const hasNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); const hasNext = frontend.includes("next"); const hasTanstackStart = frontend.includes("tanstack-start"); diff --git a/apps/cli/src/helpers/core/env-setup.ts b/apps/cli/src/helpers/core/env-setup.ts index cebd39a33..e2a1cf0c7 100644 --- a/apps/cli/src/helpers/core/env-setup.ts +++ b/apps/cli/src/helpers/core/env-setup.ts @@ -225,7 +225,8 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { } if ( - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles") ) { const nativeDir = path.join(projectDir, "apps/native"); @@ -277,7 +278,8 @@ export async function setupEnvironmentVariables(config: ProjectConfig) { const envLocalPath = path.join(convexBackendDir, ".env.local"); const hasNative = - frontend.includes("native-nativewind") || + frontend.includes("native-bare") || + frontend.includes("native-uniwind") || frontend.includes("native-unistyles"); const hasWeb = hasWebFrontend; diff --git a/apps/cli/src/helpers/core/post-installation.ts b/apps/cli/src/helpers/core/post-installation.ts index 103159b2e..8c96b455d 100644 --- a/apps/cli/src/helpers/core/post-installation.ts +++ b/apps/cli/src/helpers/core/post-installation.ts @@ -61,7 +61,8 @@ export async function displayPostInstallInstructions( ? getLintingInstructions(runCmd) : ""; const nativeInstructions = - frontend?.includes("native-nativewind") || + frontend?.includes("native-bare") || + frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles") ? getNativeInstructions(isConvex, isBackendSelf, frontend || []) : ""; @@ -103,7 +104,8 @@ export async function displayPostInstallInstructions( ].includes(f), ); const hasNative = - frontend?.includes("native-nativewind") || + frontend?.includes("native-bare") || + frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles"); const bunWebNativeWarning = diff --git a/apps/cli/src/helpers/core/template-manager.ts b/apps/cli/src/helpers/core/template-manager.ts index 6180bdc62..ec3b3f92d 100644 --- a/apps/cli/src/helpers/core/template-manager.ts +++ b/apps/cli/src/helpers/core/template-manager.ts @@ -69,9 +69,10 @@ export async function setupFrontendTemplates( const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); - const hasNativeWind = context.frontend.includes("native-nativewind"); + const hasNativeBare = context.frontend.includes("native-bare"); + const hasNativeUniwind = context.frontend.includes("native-uniwind"); const hasUnistyles = context.frontend.includes("native-unistyles"); - const _hasNative = hasNativeWind || hasUnistyles; + const _hasNative = hasNativeBare || hasNativeUniwind || hasUnistyles; const isConvex = context.backend === "convex"; if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) { @@ -201,7 +202,7 @@ export async function setupFrontendTemplates( } } - if (hasNativeWind || hasUnistyles) { + if (hasNativeBare || hasNativeUniwind || hasUnistyles) { const nativeAppDir = path.join(projectDir, "apps/native"); await fs.ensureDir(nativeAppDir); @@ -220,8 +221,10 @@ export async function setupFrontendTemplates( } let nativeFrameworkPath = ""; - if (hasNativeWind) { - nativeFrameworkPath = "nativewind"; + if (hasNativeBare) { + nativeFrameworkPath = "bare"; + } else if (hasNativeUniwind) { + nativeFrameworkPath = "uniwind"; } else if (hasUnistyles) { nativeFrameworkPath = "unistyles"; } @@ -382,9 +385,10 @@ export async function setupAuthTemplate( const hasNuxtWeb = context.frontend.includes("nuxt"); const hasSvelteWeb = context.frontend.includes("svelte"); const hasSolidWeb = context.frontend.includes("solid"); - const hasNativeWind = context.frontend.includes("native-nativewind"); + const hasNativeBare = context.frontend.includes("native-bare"); + const hasUniwind = context.frontend.includes("native-uniwind"); const hasUnistyles = context.frontend.includes("native-unistyles"); - const hasNative = hasNativeWind || hasUnistyles; + const hasNative = hasNativeBare || hasUniwind || hasUnistyles; const authProvider = context.auth; @@ -440,10 +444,9 @@ export async function setupAuthTemplate( ); } - const hasNativeWind = context.frontend.includes("native-nativewind"); - const hasUnistyles = context.frontend.includes("native-unistyles"); let nativeFrameworkPath = ""; - if (hasNativeWind) nativeFrameworkPath = "nativewind"; + if (hasNativeBare) nativeFrameworkPath = "bare"; + else if (hasUniwind) nativeFrameworkPath = "uniwind"; else if (hasUnistyles) nativeFrameworkPath = "unistyles"; if (nativeFrameworkPath) { const convexClerkNativeFrameworkSrc = path.join( @@ -529,7 +532,8 @@ export async function setupAuthTemplate( } let nativeFrameworkPath = ""; - if (hasNativeWind) nativeFrameworkPath = "nativewind"; + if (hasNativeBare) nativeFrameworkPath = "bare"; + else if (hasUniwind) nativeFrameworkPath = "uniwind"; else if (hasUnistyles) nativeFrameworkPath = "unistyles"; if (nativeFrameworkPath) { const convexBetterAuthNativeFrameworkSrc = path.join( @@ -690,8 +694,10 @@ export async function setupAuthTemplate( } let nativeFrameworkAuthPath = ""; - if (hasNativeWind) { - nativeFrameworkAuthPath = "nativewind"; + if (hasNativeBare) { + nativeFrameworkAuthPath = "bare"; + } else if (hasUniwind) { + nativeFrameworkAuthPath = "uniwind"; } else if (hasUnistyles) { nativeFrameworkAuthPath = "unistyles"; } @@ -1034,13 +1040,16 @@ export async function setupExamplesTemplate( } if (nativeAppDirExists) { - const hasNativeWind = context.frontend.includes("native-nativewind"); + const hasNativeBare = context.frontend.includes("native-bare"); + const hasUniwind = context.frontend.includes("native-uniwind"); const hasUnistyles = context.frontend.includes("native-unistyles"); - if (hasNativeWind || hasUnistyles) { + if (hasNativeBare || hasUniwind || hasUnistyles) { let nativeFramework = ""; - if (hasNativeWind) { - nativeFramework = "nativewind"; + if (hasNativeBare) { + nativeFramework = "bare"; + } else if (hasUniwind) { + nativeFramework = "uniwind"; } else if (hasUnistyles) { nativeFramework = "unistyles"; } @@ -1065,9 +1074,10 @@ export async function setupExamplesTemplate( export async function handleExtras(projectDir: string, context: ProjectConfig) { const extrasDir = path.join(PKG_ROOT, "templates/extras"); - const hasNativeWind = context.frontend.includes("native-nativewind"); + const hasNativeBare = context.frontend.includes("native-bare"); + const hasUniwind = context.frontend.includes("native-uniwind"); const hasUnistyles = context.frontend.includes("native-unistyles"); - const hasNative = hasNativeWind || hasUnistyles; + const hasNative = hasNativeBare || hasUniwind || hasUnistyles; if (context.packageManager === "pnpm") { const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml"); diff --git a/apps/cli/src/prompts/auth.ts b/apps/cli/src/prompts/auth.ts index a216d8dc4..f46cac0d7 100644 --- a/apps/cli/src/prompts/auth.ts +++ b/apps/cli/src/prompts/auth.ts @@ -18,7 +18,8 @@ export async function getAuthChoice( "tanstack-router", "tanstack-start", "next", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ].includes(f), ); @@ -29,7 +30,8 @@ export async function getAuthChoice( "tanstack-router", "tanstack-start", "next", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ].includes(f), ); diff --git a/apps/cli/src/prompts/frontend.ts b/apps/cli/src/prompts/frontend.ts index f4079c857..52282e4b9 100644 --- a/apps/cli/src/prompts/frontend.ts +++ b/apps/cli/src/prompts/frontend.ts @@ -92,8 +92,13 @@ export async function getFrontendChoice( message: "Choose native", options: [ { - value: "native-nativewind" as const, - label: "NativeWind", + value: "native-bare" as const, + label: "Bare", + hint: "Bare Expo without styling library", + }, + { + value: "native-uniwind" as const, + label: "UniWind", hint: "Use Tailwind CSS for React Native", }, { @@ -102,7 +107,7 @@ export async function getFrontendChoice( hint: "Consistent styling for React Native", }, ], - initialValue: "native-nativewind", + initialValue: "native-bare", }); if (isCancel(nativeFramework)) return exitCancelled("Operation cancelled"); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 7fb4d6c7a..4792ac343 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -27,7 +27,8 @@ export const FrontendSchema = z "tanstack-start", "next", "nuxt", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", "svelte", "solid", diff --git a/apps/cli/src/utils/better-auth-plugin-setup.ts b/apps/cli/src/utils/better-auth-plugin-setup.ts index 7327a69a5..1c3f86c23 100644 --- a/apps/cli/src/utils/better-auth-plugin-setup.ts +++ b/apps/cli/src/utils/better-auth-plugin-setup.ts @@ -32,7 +32,8 @@ export async function setupBetterAuthPlugins( } if ( - config.frontend?.includes("native-nativewind") || + config.frontend?.includes("native-bare") || + config.frontend?.includes("native-uniwind") || config.frontend?.includes("native-unistyles") ) { pluginsToAdd.push("expo()"); diff --git a/apps/cli/src/utils/compatibility-rules.ts b/apps/cli/src/utils/compatibility-rules.ts index f49e19983..742bc7ce4 100644 --- a/apps/cli/src/utils/compatibility-rules.ts +++ b/apps/cli/src/utils/compatibility-rules.ts @@ -24,7 +24,8 @@ export function splitFrontends(values: Frontend[] = []): { } { const web = values.filter((f) => isWebFrontend(f)); const native = values.filter( - (f) => f === "native-nativewind" || f === "native-unistyles", + (f) => + f === "native-bare" || f === "native-uniwind" || f === "native-unistyles", ); return { web, native }; } @@ -38,7 +39,7 @@ export function ensureSingleWebAndNative(frontends: Frontend[]) { } if (native.length > 1) { exitWithError( - "Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles", + "Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles", ); } } @@ -72,7 +73,7 @@ export function validateSelfBackendCompatibility( if (native.length > 1) { exitWithError( - "Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles", + "Cannot select multiple native frameworks. Choose only one of: native-bare, native-uniwind, native-unistyles", ); } } diff --git a/apps/cli/src/utils/config-validation.ts b/apps/cli/src/utils/config-validation.ts index cec391e8b..4bbcf5ee1 100644 --- a/apps/cli/src/utils/config-validation.ts +++ b/apps/cli/src/utils/config-validation.ts @@ -215,7 +215,8 @@ export function validateConvexConstraints( "tanstack-router", "tanstack-start", "next", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; const hasSupportedFrontend = config.frontend?.some((f) => diff --git a/apps/cli/src/utils/setup-catalogs.ts b/apps/cli/src/utils/setup-catalogs.ts index 7548c5603..e9031a7a8 100644 --- a/apps/cli/src/utils/setup-catalogs.ts +++ b/apps/cli/src/utils/setup-catalogs.ts @@ -25,7 +25,7 @@ export async function setupCatalogs( const packagePaths = [ "apps/server", "apps/web", - // "apps/native", // todo + "apps/native", "apps/fumadocs", "apps/docs", "packages/api", diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs index 022194141..4e7f277d3 100644 --- a/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/auth.ts.hbs @@ -1,5 +1,5 @@ import { createClient, type GenericCtx } from "@convex-dev/better-auth"; -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} +{{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} import { convex } from "@convex-dev/better-auth/plugins"; import { expo } from "@better-auth/expo"; {{else}} @@ -17,7 +17,7 @@ import { v } from "convex/values"; {{#if (or (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}} const siteUrl = process.env.SITE_URL!; {{/if}} -{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} +{{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} const nativeAppUrl = process.env.NATIVE_APP_URL || "mybettertapp://"; {{/if}} @@ -31,10 +31,10 @@ function createAuth( logger: { disabled: optionsOnly, }, - {{#if (and (or (includes frontend "native-nativewind") (includes frontend "native-unistyles")) (or (includes frontend "tanstack-start") (includes frontend "next")))}} + {{#if (and (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles")) (or (includes frontend "tanstack-start") (includes frontend "next")))}} baseURL: siteUrl, trustedOrigins: [siteUrl, nativeAppUrl], - {{else if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{else if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} trustedOrigins: [nativeAppUrl], {{else if (or (includes frontend "tanstack-start") (includes frontend "next"))}} baseURL: siteUrl, @@ -48,7 +48,7 @@ function createAuth( requireEmailVerification: false, }, plugins: [ - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} expo(), {{/if}} {{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}} diff --git a/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs b/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs index 248100890..7dd37e868 100644 --- a/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs +++ b/apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs @@ -3,7 +3,7 @@ import { authComponent, createAuth } from "./auth"; const http = httpRouter(); -{{#if (or (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} +{{#if (or (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} authComponent.registerRoutes(http, createAuth); {{else}} authComponent.registerRoutes(http, createAuth, { cors: true }); diff --git a/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-in.tsx.hbs new file mode 100644 index 000000000..a204392d6 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-in.tsx.hbs @@ -0,0 +1,127 @@ +import { authClient } from "@/lib/auth-client"; +import { useState } from "react"; +import { + ActivityIndicator, + Text, + TextInput, + TouchableOpacity, + View, + StyleSheet, +} from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +function SignIn() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleLogin() { + setIsLoading(true); + setError(null); + + await authClient.signIn.email( + { + email, + password, + }, + { + onError(error) { + setError(error.error?.message || "Failed to sign in"); + setIsLoading(false); + }, + onSuccess() { + setEmail(""); + setPassword(""); + }, + onFinished() { + setIsLoading(false); + }, + } + ); + } + + return ( + + Sign In + + {error ? ( + + {error} + + ) : null} + + + + + + + {isLoading ? ( + + ) : ( + Sign In + )} + + + ); +} + +const styles = StyleSheet.create({ + card: { + marginTop: 16, + padding: 16, + borderWidth: 1, + }, + title: { + fontSize: 18, + fontWeight: "bold", + marginBottom: 12, + }, + errorContainer: { + marginBottom: 12, + padding: 8, + }, + errorText: { + fontSize: 14, + }, + input: { + borderWidth: 1, + padding: 12, + fontSize: 16, + marginBottom: 12, + }, + button: { + padding: 12, + alignItems: "center", + justifyContent: "center", + }, + buttonText: { + color: "#ffffff", + fontSize: 16, + }, +}); + +export { SignIn }; + diff --git a/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-up.tsx.hbs new file mode 100644 index 000000000..b3a5d1fd4 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/native/bare/components/sign-up.tsx.hbs @@ -0,0 +1,138 @@ +import { authClient } from "@/lib/auth-client"; +import { useState } from "react"; +import { + ActivityIndicator, + Text, + TextInput, + TouchableOpacity, + View, + StyleSheet, +} from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +function SignUp() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSignUp() { + setIsLoading(true); + setError(null); + + await authClient.signUp.email( + { + name, + email, + password, + }, + { + onError(error) { + setError(error.error?.message || "Failed to sign up"); + setIsLoading(false); + }, + onSuccess() { + setName(""); + setEmail(""); + setPassword(""); + }, + onFinished() { + setIsLoading(false); + }, + } + ); + } + + return ( + + Create Account + + {error ? ( + + {error} + + ) : null} + + + + + + + + + {isLoading ? ( + + ) : ( + Sign Up + )} + + + ); +} + +const styles = StyleSheet.create({ + card: { + marginTop: 16, + padding: 16, + borderWidth: 1, + }, + title: { + fontSize: 18, + fontWeight: "bold", + marginBottom: 12, + }, + errorContainer: { + marginBottom: 12, + padding: 8, + }, + errorText: { + fontSize: 14, + }, + input: { + borderWidth: 1, + padding: 12, + fontSize: 16, + marginBottom: 12, + }, + button: { + padding: 12, + alignItems: "center", + justifyContent: "center", + }, + buttonText: { + color: "#ffffff", + fontSize: 16, + }, +}); + +export { SignUp }; + diff --git a/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-in.tsx.hbs deleted file mode 100644 index 91f66ee43..000000000 --- a/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-in.tsx.hbs +++ /dev/null @@ -1,86 +0,0 @@ -import { authClient } from "@/lib/auth-client"; -import { useState } from "react"; -import { - ActivityIndicator, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; - -export function SignIn() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleLogin = async () => { - setIsLoading(true); - setError(null); - - await authClient.signIn.email( - { - email, - password, - }, - { - onError: (error) => { - setError(error.error?.message || "Failed to sign in"); - setIsLoading(false); - }, - onSuccess: () => { - setEmail(""); - setPassword(""); - }, - onFinished: () => { - setIsLoading(false); - }, - }, - ); - }; - - return ( - - - Sign In - - - {error && ( - - {error} - - )} - - - - - - - {isLoading ? ( - - ) : ( - Sign In - )} - - - ); -} \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-up.tsx.hbs deleted file mode 100644 index 93ed2f5b0..000000000 --- a/apps/cli/templates/auth/better-auth/convex/native/nativewind/components/sign-up.tsx.hbs +++ /dev/null @@ -1,97 +0,0 @@ -import { authClient } from "@/lib/auth-client"; -import { useState } from "react"; -import { - ActivityIndicator, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; - -export function SignUp() { - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSignUp = async () => { - setIsLoading(true); - setError(null); - - await authClient.signUp.email( - { - name, - email, - password, - }, - { - onError: (error) => { - setError(error.error?.message || "Failed to sign up"); - setIsLoading(false); - }, - onSuccess: () => { - setName(""); - setEmail(""); - setPassword(""); - }, - onFinished: () => { - setIsLoading(false); - }, - }, - ); - }; - - return ( - - - Create Account - - - {error && ( - - {error} - - )} - - - - - - - - - {isLoading ? ( - - ) : ( - Sign Up - )} - - - ); -} diff --git a/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs new file mode 100644 index 000000000..9fad45969 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-in.tsx.hbs @@ -0,0 +1,91 @@ +import { authClient } from "@/lib/auth-client"; +import { useState } from "react"; +import { + ActivityIndicator, + Text, + TextInput, + Pressable, + View, +} from "react-native"; +import { Card, useThemeColor } from "heroui-native"; + +export function SignIn() { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const foregroundColor = useThemeColor("foreground"); + const dangerColor = useThemeColor("danger"); + + const handleLogin = async () => { + setIsLoading(true); + setError(null); + + await authClient.signIn.email( + { + email, + password, + }, + { + onError: (error) => { + setError(error.error?.message || "Failed to sign in"); + setIsLoading(false); + }, + onSuccess: () => { + setEmail(""); + setPassword(""); + }, + onFinished: () => { + setIsLoading(false); + }, + }, + ); + }; + + return ( + + Sign In + + {error && ( + + {error} + + )} + + + + + + + {isLoading ? ( + + ) : ( + Sign In + )} + + + ); +} + diff --git a/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs new file mode 100644 index 000000000..e983a861b --- /dev/null +++ b/apps/cli/templates/auth/better-auth/convex/native/uniwind/components/sign-up.tsx.hbs @@ -0,0 +1,102 @@ +import { authClient } from "@/lib/auth-client"; +import { useState } from "react"; +import { + ActivityIndicator, + Text, + TextInput, + Pressable, + View, +} from "react-native"; +import { Card, useThemeColor } from "heroui-native"; + +export function SignUp() { + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const foregroundColor = useThemeColor("foreground"); + const dangerColor = useThemeColor("danger"); + + const handleSignUp = async () => { + setIsLoading(true); + setError(null); + + await authClient.signUp.email( + { + name, + email, + password, + }, + { + onError: (error) => { + setError(error.error?.message || "Failed to sign up"); + setIsLoading(false); + }, + onSuccess: () => { + setName(""); + setEmail(""); + setPassword(""); + }, + onFinished: () => { + setIsLoading(false); + }, + }, + ); + }; + + return ( + + Create Account + + {error && ( + + {error} + + )} + + + + + + + + + {isLoading ? ( + + ) : ( + Sign Up + )} + + + ); +} + diff --git a/apps/cli/templates/auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs new file mode 100644 index 000000000..f2edb1070 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/bare/app/(drawer)/index.tsx.hbs @@ -0,0 +1,186 @@ +import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from "react-native"; +import { Container } from "@/components/container"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; +import { authClient } from "@/lib/auth-client"; +import { SignIn } from "@/components/sign-in"; +import { SignUp } from "@/components/sign-up"; +{{#if (eq api "orpc")}} +import { useQuery } from "@tanstack/react-query"; +import { queryClient, orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} +import { useQuery } from "@tanstack/react-query"; +import { queryClient, trpc } from "@/utils/trpc"; +{{/if}} + +export default function Home() { +const { colorScheme } = useColorScheme(); +const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; +{{#if (eq api "orpc")}} +const healthCheck = useQuery(orpc.healthCheck.queryOptions()); +const privateData = useQuery(orpc.privateData.queryOptions()); +const isConnected = healthCheck?.data === "OK"; +const isLoading = healthCheck?.isLoading; +{{/if}} +{{#if (eq api "trpc")}} +const healthCheck = useQuery(trpc.healthCheck.queryOptions()); +const privateData = useQuery(trpc.privateData.queryOptions()); +const isConnected = healthCheck?.data === "OK"; +const isLoading = healthCheck?.isLoading; +{{/if}} +const { data: session } = authClient.useSession(); + +return ( + + + + + BETTER T STACK + + + {session?.user ? ( + + + + Welcome, {session.user.name} + + + + {session.user.email} + + { + authClient.signOut(); + {{#if (eq api "orpc")}} + queryClient.invalidateQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.invalidateQueries(); + {{/if}} + }} + > + Sign Out + + + ) : null} + + {{#unless (eq api "none")}} + + + System Status + + + + + + {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} Backend + + + {isLoading + ? "Checking connection..." + : isConnected + ? "Connected to API" + : "API Disconnected"} + + + + + + + + Private Data + + {privateData && ( + + {privateData.data?.message} + + )} + + {{/unless}} + + {!session?.user && ( + <> + + + + )} + + + +); +} + +const styles = StyleSheet.create({ +scrollView: { +flex: 1, +}, +content: { +padding: 16, +}, +title: { +fontSize: 24, +fontWeight: "bold", +marginBottom: 16, +}, +userCard: { +marginBottom: 16, +padding: 16, +borderWidth: 1, +}, +userHeader: { +marginBottom: 8, +}, +userText: { +fontSize: 16, +}, +userName: { +fontWeight: "bold", +}, +userEmail: { +fontSize: 14, +marginBottom: 12, +}, +signOutButton: { +padding: 12, +}, +signOutText: { +color: "#ffffff", +}, +statusCard: { +marginBottom: 16, +padding: 16, +borderWidth: 1, +}, +cardTitle: { +fontSize: 16, +fontWeight: "bold", +marginBottom: 12, +}, +statusRow: { +flexDirection: "row", +alignItems: "center", +gap: 8, +}, +statusIndicator: { +height: 8, +width: 8, +}, +statusContent: { +flex: 1, +}, +statusTitle: { +fontSize: 14, +fontWeight: "bold", +}, +statusText: { +fontSize: 12, +}, +privateDataCard: { +marginBottom: 16, +padding: 16, +borderWidth: 1, +}, +privateDataText: { +fontSize: 14, +}, +}); \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/native/bare/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/native/bare/components/sign-in.tsx.hbs new file mode 100644 index 000000000..22dc10bf2 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/bare/components/sign-in.tsx.hbs @@ -0,0 +1,131 @@ +import { authClient } from "@/lib/auth-client"; +{{#if (eq api "trpc")}} +import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { queryClient } from "@/utils/orpc"; +{{/if}} +import { useState } from "react"; +import { +ActivityIndicator, +Text, +TextInput, +TouchableOpacity, +View, +StyleSheet, +} from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +function SignIn() { +const { colorScheme } = useColorScheme(); +const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; +const [form, setForm] = useState({ email: "", password: "" }); +const [isLoading, setIsLoading] = useState(false); +const [error, setError] = useState(null); + + function handleFormChange(field: "email" | "password", value: string) { + setForm(prev => ({ ...prev, [field]: value })); + } + + async function handleLogin() { + setIsLoading(true); + setError(null); + + await authClient.signIn.email( + { + email: form.email, + password: form.password, + }, + { + onError(error) { + setError(error.error?.message || "Failed to sign in"); + setIsLoading(false); + }, + onSuccess() { + setForm({ email: "", password: "" }); + {{#if (eq api "orpc")}} + queryClient.refetchQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.refetchQueries(); + {{/if}} + }, + onFinished() { + setIsLoading(false); + }, + } + ); + } + + return ( + + Sign In + + {error ? ( + + {error} + + ) : null} + + handleFormChange("email", value)} + keyboardType="email-address" + autoCapitalize="none" + /> + + handleFormChange("password", value)} + secureTextEntry + /> + + + {isLoading ? ( + + ) : ( + Sign In + )} + + + ); + } + + const styles = StyleSheet.create({ + card: { + marginTop: 16, + padding: 16, + borderWidth: 1, + }, + title: { + fontSize: 18, + fontWeight: "bold", + marginBottom: 12, + }, + errorContainer: { + marginBottom: 12, + padding: 8, + }, + errorText: { + fontSize: 14, + }, + input: { + borderWidth: 1, + padding: 12, + fontSize: 16, + marginBottom: 12, + }, + button: { + padding: 12, + alignItems: "center", + justifyContent: "center", + }, + buttonText: { + color: "#ffffff", + fontSize: 16, + }, + }); + + export { SignIn }; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/native/bare/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/native/bare/components/sign-up.tsx.hbs new file mode 100644 index 000000000..358293929 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/bare/components/sign-up.tsx.hbs @@ -0,0 +1,150 @@ +import { authClient } from "@/lib/auth-client"; +{{#if (eq api "trpc")}} +import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { queryClient } from "@/utils/orpc"; +{{/if}} +import { useState } from "react"; +import { + ActivityIndicator, + Text, + TextInput, + TouchableOpacity, + View, + StyleSheet, +} from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +function SignUp() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSignUp() { + setIsLoading(true); + setError(null); + + await authClient.signUp.email( + { + name, + email, + password, + }, + { + onError(error) { + setError(error.error?.message || "Failed to sign up"); + setIsLoading(false); + }, + onSuccess() { + setName(""); + setEmail(""); + setPassword(""); + {{#if (eq api "orpc")}} + queryClient.refetchQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.refetchQueries(); + {{/if}} + }, + onFinished() { + setIsLoading(false); + }, + } + ); + } + + return ( + + Create Account + + {error ? ( + + {error} + + ) : null} + + + + + + + + + {isLoading ? ( + + ) : ( + Sign Up + )} + + + ); +} + +const styles = StyleSheet.create({ + card: { + marginTop: 16, + padding: 16, + borderWidth: 1, + }, + title: { + fontSize: 18, + fontWeight: "bold", + marginBottom: 12, + }, + errorContainer: { + marginBottom: 12, + padding: 8, + }, + errorText: { + fontSize: 14, + }, + input: { + borderWidth: 1, + padding: 12, + fontSize: 16, + marginBottom: 12, + }, + button: { + padding: 12, + alignItems: "center", + justifyContent: "center", + }, + buttonText: { + color: "#ffffff", + fontSize: 16, + }, +}); + +export { SignUp }; + diff --git a/apps/cli/templates/auth/better-auth/native/nativewind/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/better-auth/native/nativewind/app/(drawer)/index.tsx.hbs deleted file mode 100644 index f6954b616..000000000 --- a/apps/cli/templates/auth/better-auth/native/nativewind/app/(drawer)/index.tsx.hbs +++ /dev/null @@ -1,95 +0,0 @@ -import { authClient } from "@/lib/auth-client"; -import { useQuery } from "@tanstack/react-query"; -import { ScrollView, Text, TouchableOpacity, View } from "react-native"; - -import { Container } from "@/components/container"; -import { SignIn } from "@/components/sign-in"; -import { SignUp } from "@/components/sign-up"; -{{#if (eq api "orpc")}} -import { queryClient, orpc } from "@/utils/orpc"; -{{/if}} -{{#if (eq api "trpc")}} -import { queryClient, trpc } from "@/utils/trpc"; -{{/if}} - -export default function Home() { - {{#if (eq api "orpc")}} - const healthCheck = useQuery(orpc.healthCheck.queryOptions()); - const privateData = useQuery(orpc.privateData.queryOptions()); - {{/if}} - {{#if (eq api "trpc")}} - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); - const privateData = useQuery(trpc.privateData.queryOptions()); - {{/if}} - const { data: session } = authClient.useSession(); - - return ( - - - - - BETTER T STACK - - {session?.user ? ( - - - - Welcome,{" "} - {session.user.name} - - - - {session.user.email} - - - { - authClient.signOut(); - queryClient.invalidateQueries(); - }} - > - Sign Out - - - ) : null} - - API Status - - - - {healthCheck.isLoading - ? "Checking..." - : healthCheck.data - ? "Connected to API" - : "API Disconnected"} - - - - - - Private Data - - {privateData && ( - - - {privateData.data?.message} - - - )} - - {!session?.user && ( - <> - - - - )} - - - - ); -} diff --git a/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-in.tsx.hbs deleted file mode 100644 index 244627b81..000000000 --- a/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-in.tsx.hbs +++ /dev/null @@ -1,93 +0,0 @@ -import { authClient } from "@/lib/auth-client"; -{{#if (eq api "trpc")}} -import { queryClient } from "@/utils/trpc"; -{{/if}} -{{#if (eq api "orpc")}} -import { queryClient } from "@/utils/orpc"; -{{/if}} -import { useState } from "react"; -import { - ActivityIndicator, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; - -export function SignIn() { - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleLogin = async () => { - setIsLoading(true); - setError(null); - - await authClient.signIn.email( - { - email, - password, - }, - { - onError: (error) => { - setError(error.error?.message || "Failed to sign in"); - setIsLoading(false); - }, - onSuccess: () => { - setEmail(""); - setPassword(""); - queryClient.refetchQueries(); - }, - onFinished: () => { - setIsLoading(false); - }, - }, - ); - }; - - return ( - - - Sign In - - - {error && ( - - {error} - - )} - - - - - - - {isLoading ? ( - - ) : ( - Sign In - )} - - - ); -} \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-up.tsx.hbs deleted file mode 100644 index bccd9c73e..000000000 --- a/apps/cli/templates/auth/better-auth/native/nativewind/components/sign-up.tsx.hbs +++ /dev/null @@ -1,104 +0,0 @@ -import { authClient } from "@/lib/auth-client"; -{{#if (eq api "trpc")}} -import { queryClient } from "@/utils/trpc"; -{{/if}} -{{#if (eq api "orpc")}} -import { queryClient } from "@/utils/orpc"; -{{/if}} -import { useState } from "react"; -import { - ActivityIndicator, - Text, - TextInput, - TouchableOpacity, - View, -} from "react-native"; - -export function SignUp() { - const [name, setName] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSignUp = async () => { - setIsLoading(true); - setError(null); - - await authClient.signUp.email( - { - name, - email, - password, - }, - { - onError: (error) => { - setError(error.error?.message || "Failed to sign up"); - setIsLoading(false); - }, - onSuccess: () => { - setName(""); - setEmail(""); - setPassword(""); - queryClient.refetchQueries(); - }, - onFinished: () => { - setIsLoading(false); - }, - }, - ); - }; - - return ( - - - Create Account - - - {error && ( - - {error} - - )} - - - - - - - - - {isLoading ? ( - - ) : ( - Sign Up - )} - - - ); -} diff --git a/apps/cli/templates/auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs index b7c6193c6..184ee1219 100644 --- a/apps/cli/templates/auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs +++ b/apps/cli/templates/auth/better-auth/native/unistyles/app/(drawer)/index.tsx.hbs @@ -1,5 +1,4 @@ import { authClient } from "@/lib/auth-client"; -import { useQuery } from "@tanstack/react-query"; import { ScrollView, Text, TouchableOpacity, View } from "react-native"; import { StyleSheet } from "react-native-unistyles"; @@ -7,9 +6,11 @@ import { Container } from "@/components/container"; import { SignIn } from "@/components/sign-in"; import { SignUp } from "@/components/sign-up"; {{#if (eq api "orpc")}} +import { useQuery } from "@tanstack/react-query"; import { queryClient, orpc } from "@/utils/orpc"; {{/if}} {{#if (eq api "trpc")}} +import { useQuery } from "@tanstack/react-query"; import { queryClient, trpc } from "@/utils/trpc"; {{/if}} @@ -43,13 +44,19 @@ export default function Home() { style={styles.signOutButton} onPress={() => { authClient.signOut(); + {{#if (eq api "orpc")}} + queryClient.invalidateQueries(); + {{/if}} + {{#if (eq api "trpc")}} queryClient.invalidateQueries(); + {{/if}} }} > Sign Out ) : null} + {{#unless (eq api "none")}} API Status @@ -80,6 +87,7 @@ export default function Home() { )} + {{/unless}} {!session?.user && ( <> diff --git a/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-in.tsx.hbs index c63395bd6..721584309 100644 --- a/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-in.tsx.hbs +++ b/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-in.tsx.hbs @@ -38,7 +38,12 @@ export function SignIn() { onSuccess: () => { setEmail(""); setPassword(""); + {{#if (eq api "orpc")}} queryClient.refetchQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.refetchQueries(); + {{/if}} }, onFinished: () => { setIsLoading(false); diff --git a/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-up.tsx.hbs index 67f0a1baa..502e49d73 100644 --- a/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-up.tsx.hbs +++ b/apps/cli/templates/auth/better-auth/native/unistyles/components/sign-up.tsx.hbs @@ -41,7 +41,12 @@ export function SignUp() { setName(""); setEmail(""); setPassword(""); + {{#if (eq api "orpc")}} queryClient.refetchQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.refetchQueries(); + {{/if}} }, onFinished: () => { setIsLoading(false); diff --git a/apps/cli/templates/auth/better-auth/native/uniwind/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/better-auth/native/uniwind/app/(drawer)/index.tsx.hbs new file mode 100644 index 000000000..1288bb96d --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/uniwind/app/(drawer)/index.tsx.hbs @@ -0,0 +1,123 @@ +import { Text, View, Pressable } from "react-native"; +import { Container } from "@/components/container"; +import { authClient } from "@/lib/auth-client"; +import { Ionicons } from "@expo/vector-icons"; +import { Card, Chip, useThemeColor } from "heroui-native"; +import { SignIn } from "@/components/sign-in"; +import { SignUp } from "@/components/sign-up"; +{{#if (eq api "orpc")}} +import { useQuery } from "@tanstack/react-query"; +import { queryClient, orpc } from "@/utils/orpc"; +{{/if}} +{{#if (eq api "trpc")}} +import { useQuery } from "@tanstack/react-query"; +import { queryClient, trpc } from "@/utils/trpc"; +{{/if}} + +export default function Home() { +{{#if (eq api "orpc")}} +const healthCheck = useQuery(orpc.healthCheck.queryOptions()); +const privateData = useQuery(orpc.privateData.queryOptions()); +const isConnected = healthCheck?.data === "OK"; +const isLoading = healthCheck?.isLoading; +{{/if}} +{{#if (eq api "trpc")}} +const healthCheck = useQuery(trpc.healthCheck.queryOptions()); +const privateData = useQuery(trpc.privateData.queryOptions()); +const isConnected = healthCheck?.data === "OK"; +const isLoading = healthCheck?.isLoading; +{{/if}} +const { data: session } = authClient.useSession(); + +const mutedColor = useThemeColor("muted"); +const successColor = useThemeColor("success"); +const dangerColor = useThemeColor("danger"); +const foregroundColor = useThemeColor("foreground"); + +return ( + + + + BETTER T STACK + + + + {session?.user ? ( + + + Welcome, {session.user.name} + + + {session.user.email} + + { + authClient.signOut(); + {{#if (eq api "orpc")}} + queryClient.invalidateQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.invalidateQueries(); + {{/if}} + }} + > + Sign Out + + + ) : null} + + {{#unless (eq api "none")}} + + + System Status + + {isConnected ? "LIVE" : "OFFLINE"} + + + + + + + + + {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} Backend + + + {isLoading + ? "Checking connection..." + : isConnected + ? "Connected to API" + : "API Disconnected"} + + + {isLoading && ( + + )} + {!isLoading && isConnected && ( + + )} + {!isLoading && !isConnected && ( + + )} + + + + + + Private Data + {privateData && ( + + {privateData.data?.message} + + )} + + {{/unless}} + + {!session?.user && ( + <> + + + + )} + +); +} \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-in.tsx.hbs b/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-in.tsx.hbs new file mode 100644 index 000000000..f6ab4d43f --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-in.tsx.hbs @@ -0,0 +1,90 @@ +import { authClient } from "@/lib/auth-client"; +{{#if (eq api "trpc")}} +import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { queryClient } from "@/utils/orpc"; +{{/if}} +import { useState } from "react"; +import { +ActivityIndicator, +Text, +TextInput, +Pressable, +View, +} from "react-native"; +import { Card, useThemeColor } from "heroui-native"; + +function SignIn() { +const [email, setEmail] = useState(""); +const [password, setPassword] = useState(""); +const [isLoading, setIsLoading] = useState(false); +const [error, setError] = useState(null); + + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const foregroundColor = useThemeColor("foreground"); + const dangerColor = useThemeColor("danger"); + + async function handleLogin() { + setIsLoading(true); + setError(null); + + await authClient.signIn.email( + { + email, + password, + }, + { + onError(error) { + setError(error.error?.message || "Failed to sign in"); + setIsLoading(false); + }, + onSuccess() { + setEmail(""); + setPassword(""); + {{#if (eq api "orpc")}} + queryClient.refetchQueries(); + {{/if}} + {{#if (eq api "trpc")}} + queryClient.refetchQueries(); + {{/if}} + }, + onFinished() { + setIsLoading(false); + }, + } + ); + } + + return ( + + Sign In + + {error ? ( + + {error} + + ) : null} + + + + + + + {isLoading ? ( + + ) : ( + Sign In + )} + + + ); + } + + export { SignIn }; \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-up.tsx.hbs b/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-up.tsx.hbs new file mode 100644 index 000000000..0e529d3a7 --- /dev/null +++ b/apps/cli/templates/auth/better-auth/native/uniwind/components/sign-up.tsx.hbs @@ -0,0 +1,116 @@ +import { authClient } from "@/lib/auth-client"; +{{#if (eq api "trpc")}} +import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { queryClient } from "@/utils/orpc"; +{{/if}} +import { useState } from "react"; +import { +ActivityIndicator, +Text, +TextInput, +Pressable, +View, +} from "react-native"; +import { Card, useThemeColor } from "heroui-native"; + +function signUpHandler({ +name, +email, +password, +setError, +setIsLoading, +setName, +setEmail, +setPassword, +}) { +setIsLoading(true); +setError(null); + +authClient.signUp.email( +{ +name, +email, +password, +}, +{ +onError(error) { +setError(error.error?.message || "Failed to sign up"); +setIsLoading(false); +}, +onSuccess() { +setName(""); +setEmail(""); +setPassword(""); +{{#if (eq api "orpc")}} +queryClient.refetchQueries(); +{{/if}} +{{#if (eq api "trpc")}} +queryClient.refetchQueries(); +{{/if}} +}, +onFinished() { +setIsLoading(false); +}, +} +); +} + +export function SignUp() { +const [name, setName] = useState(""); +const [email, setEmail] = useState(""); +const [password, setPassword] = useState(""); +const [isLoading, setIsLoading] = useState(false); +const [error, setError] = useState(null); + + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const foregroundColor = useThemeColor("foreground"); + const dangerColor = useThemeColor("danger"); + + function handlePress() { + signUpHandler({ + name, + email, + password, + setError, + setIsLoading, + setName, + setEmail, + setPassword, + }); + } + + return ( + + Create Account + + {error && ( + + {error} + + )} + + + + + + + + + {isLoading ? ( + + ) : ( + Sign Up + )} + + + ); + } \ No newline at end of file diff --git a/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs b/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs index 3924685a0..c386f36bb 100644 --- a/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs +++ b/apps/cli/templates/auth/better-auth/server/base/src/index.ts.hbs @@ -16,7 +16,7 @@ export const auth = betterAuth({ }), trustedOrigins: [ process.env.CORS_ORIGIN || "", - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} "mybettertapp://", "exp://" {{/if}} ], @@ -77,7 +77,7 @@ export const auth = betterAuth({ }), trustedOrigins: [ process.env.CORS_ORIGIN || "", - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} "mybettertapp://", "exp://" {{/if}} ], @@ -138,7 +138,7 @@ export const auth = betterAuth({ }), trustedOrigins: [ env.CORS_ORIGIN, - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} "mybettertapp://", "exp://" {{/if}} ], @@ -206,7 +206,7 @@ export const auth = betterAuth({ database: mongodbAdapter(client), trustedOrigins: [ process.env.CORS_ORIGIN || "", - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} "mybettertapp://", "exp://" {{/if}} ], @@ -258,7 +258,7 @@ export const auth = betterAuth({ database: "", // Invalid configuration trustedOrigins: [ process.env.CORS_ORIGIN || "", - {{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} + {{#if (or (includes frontend "native-bare") (includes frontend "native-uniwind") (includes frontend "native-unistyles"))}} "mybettertapp://", "exp://" {{/if}} ], diff --git a/apps/cli/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs b/apps/cli/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs new file mode 100644 index 000000000..7a754c224 --- /dev/null +++ b/apps/cli/templates/examples/ai/native/bare/app/(drawer)/ai.tsx.hbs @@ -0,0 +1,287 @@ +import { useRef, useEffect, useState } from "react"; +import { + View, + Text, + TextInput, + TouchableOpacity, + ScrollView, + KeyboardAvoidingView, + Platform, + StyleSheet, +} from "react-native"; +import { useChat } from "@ai-sdk/react"; +import { DefaultChatTransport } from "ai"; +import { fetch as expoFetch } from "expo/fetch"; +import { Ionicons } from "@expo/vector-icons"; +import { Container } from "@/components/container"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +const generateAPIUrl = (relativePath: string) => { + const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; + if (!serverUrl) { + throw new Error( + "EXPO_PUBLIC_SERVER_URL environment variable is not defined" + ); + } + const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`; + return serverUrl.concat(path); +}; + +export default function AIScreen() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + const [input, setInput] = useState(""); + const { messages, error, sendMessage } = useChat({ + transport: new DefaultChatTransport({ + fetch: expoFetch as unknown as typeof globalThis.fetch, + api: generateAPIUrl("/ai"), + }), + onError: (error) => console.error(error, "AI Chat Error"), + }); + const scrollViewRef = useRef(null); + + useEffect(() => { + scrollViewRef.current?.scrollToEnd({ animated: true }); + }, [messages]); + + function onSubmit() { + const value = input.trim(); + if (value) { + sendMessage({ text: value }); + setInput(""); + } + } + + if (error) { + return ( + + + + + Error: {error.message} + + + Please check your connection and try again. + + + + + ); + } + + return ( + + + + + + AI Chat + + + Chat with our AI assistant + + + + {messages.length === 0 ? ( + + + Ask me anything to get started! + + + ) : ( + + {messages.map((message) => ( + + + {message.role === "user" ? "You" : "AI Assistant"} + + + {message.parts.map((part, i) => + part.type === "text" ? ( + + {part.text} + + ) : ( + + {JSON.stringify(part)} + + ) + )} + + + ))} + + )} + + + + { + e.preventDefault(); + onSubmit(); + }} + autoFocus={true} + multiline + /> + + + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + content: { + flex: 1, + padding: 16, + }, + header: { + marginBottom: 16, + }, + headerTitle: { + fontSize: 20, + fontWeight: "bold", + marginBottom: 4, + }, + headerSubtitle: { + fontSize: 14, + }, + scrollView: { + flex: 1, + marginBottom: 16, + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + emptyText: { + fontSize: 16, + textAlign: "center", + }, + messagesList: { + gap: 8, + paddingBottom: 16, + }, + messageCard: { + borderWidth: 1, + padding: 12, + maxWidth: "80%", + }, + messageRole: { + fontSize: 12, + fontWeight: "bold", + marginBottom: 4, + }, + messageParts: { + gap: 4, + }, + messageText: { + fontSize: 14, + lineHeight: 20, + }, + inputContainer: { + borderTopWidth: 1, + paddingTop: 12, + }, + inputRow: { + flexDirection: "row", + alignItems: "flex-end", + gap: 8, + }, + input: { + flex: 1, + borderWidth: 1, + padding: 8, + fontSize: 14, + minHeight: 36, + maxHeight: 100, + }, + sendButton: { + padding: 8, + justifyContent: "center", + alignItems: "center", + }, + errorContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + errorCard: { + borderWidth: 1, + padding: 16, + }, + errorTitle: { + fontSize: 16, + fontWeight: "bold", + marginBottom: 8, + textAlign: "center", + }, + errorText: { + fontSize: 14, + textAlign: "center", + }, +}); + diff --git a/apps/cli/templates/examples/ai/native/nativewind/polyfills.js b/apps/cli/templates/examples/ai/native/bare/polyfills.js similarity index 99% rename from apps/cli/templates/examples/ai/native/nativewind/polyfills.js rename to apps/cli/templates/examples/ai/native/bare/polyfills.js index 8e2e56f50..8bbb2b269 100644 --- a/apps/cli/templates/examples/ai/native/nativewind/polyfills.js +++ b/apps/cli/templates/examples/ai/native/bare/polyfills.js @@ -23,3 +23,4 @@ if (Platform.OS !== "web") { } export {}; + diff --git a/apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs b/apps/cli/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs similarity index 59% rename from apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs rename to apps/cli/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs index e68089c64..6c3f9c260 100644 --- a/apps/cli/templates/examples/ai/native/nativewind/app/(drawer)/ai.tsx.hbs +++ b/apps/cli/templates/examples/ai/native/uniwind/app/(drawer)/ai.tsx.hbs @@ -3,7 +3,7 @@ import { View, Text, TextInput, - TouchableOpacity, + Pressable, ScrollView, KeyboardAvoidingView, Platform, @@ -13,14 +13,16 @@ import { DefaultChatTransport } from "ai"; import { fetch as expoFetch } from "expo/fetch"; import { Ionicons } from "@expo/vector-icons"; import { Container } from "@/components/container"; +import { Card, useThemeColor } from "heroui-native"; const generateAPIUrl = (relativePath: string) => { const serverUrl = process.env.EXPO_PUBLIC_SERVER_URL; if (!serverUrl) { - throw new Error("EXPO_PUBLIC_SERVER_URL environment variable is not defined"); + throw new Error( + "EXPO_PUBLIC_SERVER_URL environment variable is not defined" + ); } - - const path = relativePath.startsWith('/') ? relativePath : `/${relativePath}`; + const path = relativePath.startsWith("/") ? relativePath : `/${relativePath}`; return serverUrl.concat(path); }; @@ -29,12 +31,15 @@ export default function AIScreen() { const { messages, error, sendMessage } = useChat({ transport: new DefaultChatTransport({ fetch: expoFetch as unknown as typeof globalThis.fetch, - api: generateAPIUrl('/ai'), + api: generateAPIUrl("/ai"), }), - onError: error => console.error(error, 'AI Chat Error'), + onError: (error) => console.error(error, "AI Chat Error"), }); - const scrollViewRef = useRef(null); + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const foregroundColor = useThemeColor("foreground"); + const dangerColor = useThemeColor("danger"); useEffect(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); @@ -52,12 +57,14 @@ export default function AIScreen() { return ( - - Error: {error.message} - - - Please check your connection and try again. - + + + Error: {error.message} + + + Please check your connection and try again. + + ); @@ -65,7 +72,7 @@ export default function AIScreen() { return ( - @@ -74,11 +81,10 @@ export default function AIScreen() { AI Chat - + Chat with our AI assistant - {messages.length === 0 ? ( - + Ask me anything to get started! ) : ( - + {messages.map((message) => ( - {message.role === "user" ? "You" : "AI Assistant"} - - {message.parts.map((part, i) => { - if (part.type === 'text') { - return ( - - {part.text} - - ); - } - return ( + + {message.parts.map((part, i) => + part.type === "text" ? ( + + {part.text} + + ) : ( {JSON.stringify(part)} - ); - })} + ) + )} - + ))} )} - - - + + { e.preventDefault(); onSubmit(); }} autoFocus={true} /> - - + ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/cli/templates/examples/ai/native/uniwind/polyfills.js b/apps/cli/templates/examples/ai/native/uniwind/polyfills.js new file mode 100644 index 000000000..8bbb2b269 --- /dev/null +++ b/apps/cli/templates/examples/ai/native/uniwind/polyfills.js @@ -0,0 +1,26 @@ +import structuredClone from "@ungap/structured-clone"; +import { Platform } from "react-native"; + +if (Platform.OS !== "web") { + const setupPolyfills = async () => { + const { polyfillGlobal } = await import( + "react-native/Libraries/Utilities/PolyfillFunctions" + ); + + const { TextEncoderStream, TextDecoderStream } = await import( + "@stardazed/streams-text-encoding" + ); + + if (!("structuredClone" in global)) { + polyfillGlobal("structuredClone", () => structuredClone); + } + + polyfillGlobal("TextEncoderStream", () => TextEncoderStream); + polyfillGlobal("TextDecoderStream", () => TextDecoderStream); + }; + + setupPolyfills(); +} + +export {}; + diff --git a/apps/cli/templates/examples/todo/native/bare/app/(drawer)/todos.tsx.hbs b/apps/cli/templates/examples/todo/native/bare/app/(drawer)/todos.tsx.hbs new file mode 100644 index 000000000..11aba1ec6 --- /dev/null +++ b/apps/cli/templates/examples/todo/native/bare/app/(drawer)/todos.tsx.hbs @@ -0,0 +1,521 @@ +import { useState } from "react"; +import { + View, + Text, + TextInput, + ScrollView, + ActivityIndicator, + Alert, + TouchableOpacity, + StyleSheet, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +{{#if (eq backend "convex")}} +import { useMutation, useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; +{{else}} +import { useMutation, useQuery } from "@tanstack/react-query"; +{{/if}} +import { Container } from "@/components/container"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; +{{#unless (eq backend "convex")}} + {{#if (eq api "orpc")}} +import { orpc } from "@/utils/orpc"; + {{/if}} + {{#if (eq api "trpc")}} +import { trpc } from "@/utils/trpc"; + {{/if}} +{{/unless}} + +export default function TodosScreen() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + const [newTodoText, setNewTodoText] = useState(""); + + {{#if (eq backend "convex")}} + const todos = useQuery(api.todos.getAll); + const createTodoMutation = useMutation(api.todos.create); + const toggleTodoMutation = useMutation(api.todos.toggle); + const deleteTodoMutation = useMutation(api.todos.deleteTodo); + + async function handleAddTodo() { + const text = newTodoText.trim(); + if (!text) return; + await createTodoMutation({ text }); + setNewTodoText(""); + } + + function handleToggleTodo(id: Id<"todos">, currentCompleted: boolean) { + toggleTodoMutation({ id, completed: !currentCompleted }); + } + + function handleDeleteTodo(id: Id<"todos">) { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteTodoMutation({ id }), + }, + ]); + } + + const isLoading = !todos; + const completedCount = todos?.filter((t) => t.completed).length || 0; + const totalCount = todos?.length || 0; + {{else}} + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }) + ); + const toggleMutation = useMutation( + orpc.todo.toggle.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + }) + ); + const deleteMutation = useMutation( + orpc.todo.delete.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + }) + ); + {{/if}} + {{#if (eq api "trpc")}} + const todos = useQuery(trpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + trpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }) + ); + const toggleMutation = useMutation( + trpc.todo.toggle.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + }) + ); + const deleteMutation = useMutation( + trpc.todo.delete.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + }) + ); + {{/if}} + + function handleAddTodo() { + if (newTodoText.trim()) { + createMutation.mutate({ text: newTodoText }); + } + } + + function handleToggleTodo(id: number, completed: boolean) { + toggleMutation.mutate({ id, completed: !completed }); + } + + function handleDeleteTodo(id: number) { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteMutation.mutate({ id }), + }, + ]); + } + + const isLoading = todos?.isLoading; + const completedCount = todos?.data?.filter((t) => t.completed).length || 0; + const totalCount = todos?.data?.length || 0; + {{/if}} + + return ( + + + + + + Todo List + + {totalCount > 0 && ( + + + {completedCount}/{totalCount} + + + )} + + + + + + + + + + {{else}} + disabled={createMutation.isPending || !newTodoText.trim()} + style={[ + styles.addButton, + { + backgroundColor: + createMutation.isPending || !newTodoText.trim() + ? theme.border + : theme.primary, + opacity: + createMutation.isPending || !newTodoText.trim() ? 0.5 : 1, + }, + ]} + > + {createMutation.isPending ? ( + + ) : ( + + )} + {{/if}} + + + + + {{#if (eq backend "convex")}} + {isLoading && ( + + + + Loading todos... + + + )} + + {todos && todos.length === 0 && !isLoading && ( + + + + No todos yet + + + Add your first task to get started! + + + )} + + {todos && todos.length > 0 && ( + + {todos.map((todo) => ( + + + handleToggleTodo(todo._id, todo.completed)} + style={[styles.checkbox, { borderColor: theme.border }]} + > + {todo.completed && ( + + )} + + + + {todo.text} + + + handleDeleteTodo(todo._id)} + style={styles.deleteButton} + > + + + + + ))} + + )} + {{else}} + {isLoading && ( + + + + Loading todos... + + + )} + + {todos?.data && todos.data.length === 0 && !isLoading && ( + + + + No todos yet + + + Add your first task to get started! + + + )} + + {todos?.data && todos.data.length > 0 && ( + + {todos.data.map((todo) => ( + + + handleToggleTodo(todo.id, todo.completed)} + style={[styles.checkbox, { borderColor: theme.border }]} + > + {todo.completed && ( + + )} + + + + {todo.text} + + + handleDeleteTodo(todo.id)} + style={styles.deleteButton} + > + + + + + ))} + + )} + {{/if}} + + + ); +} + +const styles = StyleSheet.create({ + scrollView: { + flex: 1, + }, + contentContainer: { + padding: 16, + }, + header: { + marginBottom: 16, + }, + headerRow: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + }, + title: { + fontSize: 24, + fontWeight: "bold", + }, + badge: { + paddingHorizontal: 8, + paddingVertical: 4, + }, + badgeText: { + color: "#ffffff", + fontSize: 12, + }, + inputCard: { + borderWidth: 1, + padding: 12, + marginBottom: 16, + }, + inputRow: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + inputContainer: { + flex: 1, + }, + input: { + borderWidth: 1, + padding: 12, + fontSize: 16, + }, + addButton: { + padding: 12, + justifyContent: "center", + alignItems: "center", + }, + centerContainer: { + alignItems: "center", + justifyContent: "center", + paddingVertical: 32, + }, + loadingText: { + marginTop: 16, + fontSize: 14, + }, + emptyCard: { + borderWidth: 1, + padding: 32, + alignItems: "center", + justifyContent: "center", + }, + emptyTitle: { + fontSize: 16, + fontWeight: "bold", + marginBottom: 8, + }, + emptyText: { + fontSize: 14, + textAlign: "center", + }, + todosList: { + gap: 8, + }, + todoCard: { + borderWidth: 1, + padding: 12, + }, + todoRow: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + checkbox: { + width: 20, + height: 20, + borderWidth: 2, + justifyContent: "center", + alignItems: "center", + }, + todoTextContainer: { + flex: 1, + }, + todoText: { + fontSize: 16, + }, + deleteButton: { + padding: 8, + }, +}); \ No newline at end of file diff --git a/apps/cli/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs b/apps/cli/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs deleted file mode 100644 index f2bc8fbdf..000000000 --- a/apps/cli/templates/examples/todo/native/nativewind/app/(drawer)/todos.tsx.hbs +++ /dev/null @@ -1,295 +0,0 @@ -import { useState } from "react"; -import { - View, - Text, - TextInput, - TouchableOpacity, - ScrollView, - ActivityIndicator, - Alert, -} from "react-native"; -import { Ionicons } from "@expo/vector-icons"; -{{#if (eq backend "convex")}} -import { useMutation, useQuery } from "convex/react"; -import { api } from "@{{projectName}}/backend/convex/_generated/api"; -import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; -{{else}} -import { useMutation, useQuery } from "@tanstack/react-query"; -{{/if}} - -import { Container } from "@/components/container"; -{{#unless (eq backend "convex")}} -{{#if (eq api "orpc")}} -import { orpc } from "@/utils/orpc"; -{{/if}} -{{#if (eq api "trpc")}} -import { trpc } from "@/utils/trpc"; -{{/if}} -{{/unless}} - -export default function TodosScreen() { - const [newTodoText, setNewTodoText] = useState(""); - - {{#if (eq backend "convex")}} - const todos = useQuery(api.todos.getAll); - const createTodoMutation = useMutation(api.todos.create); - const toggleTodoMutation = useMutation(api.todos.toggle); - const deleteTodoMutation = useMutation(api.todos.deleteTodo); - - const handleAddTodo = async () => { - const text = newTodoText.trim(); - if (!text) return; - await createTodoMutation({ text }); - setNewTodoText(""); - }; - - const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => { - toggleTodoMutation({ id, completed: !currentCompleted }); - }; - - const handleDeleteTodo = (id: Id<"todos">) => { - Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: () => deleteTodoMutation({ id }), - }, - ]); - }; - {{else}} - {{#if (eq api "orpc")}} - const todos = useQuery(orpc.todo.getAll.queryOptions()); - const createMutation = useMutation( - orpc.todo.create.mutationOptions({ - onSuccess: () => { - todos.refetch(); - setNewTodoText(""); - }, - }), - ); - const toggleMutation = useMutation( - orpc.todo.toggle.mutationOptions({ - onSuccess: () => { todos.refetch() }, - }), - ); - const deleteMutation = useMutation( - orpc.todo.delete.mutationOptions({ - onSuccess: () => { todos.refetch() }, - }), - ); - {{/if}} - {{#if (eq api "trpc")}} - const todos = useQuery(trpc.todo.getAll.queryOptions()); - const createMutation = useMutation( - trpc.todo.create.mutationOptions({ - onSuccess: () => { - todos.refetch(); - setNewTodoText(""); - }, - }), - ); - const toggleMutation = useMutation( - trpc.todo.toggle.mutationOptions({ - onSuccess: () => { todos.refetch() }, - }), - ); - const deleteMutation = useMutation( - trpc.todo.delete.mutationOptions({ - onSuccess: () => { todos.refetch() }, - }), - ); - {{/if}} - - const handleAddTodo = () => { - if (newTodoText.trim()) { - createMutation.mutate({ text: newTodoText }); - } - }; - - const handleToggleTodo = (id: number, completed: boolean) => { - toggleMutation.mutate({ id, completed: !completed }); - }; - - const handleDeleteTodo = (id: number) => { - Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - style: "destructive", - onPress: () => deleteMutation.mutate({ id }), - }, - ]); - }; - {{/if}} - - return ( - - - - - - Todo List - - - Manage your tasks efficiently - - - - - - - {{#if (eq backend "convex")}} - Add - {{else}} - {createMutation.isPending ? ( - - ) : ( - Add - )} - {{/if}} - - - - - {{#if (eq backend "convex")}} - {todos === undefined ? ( - - - - ) : todos.length === 0 ? ( - - No todos yet. Add one above! - - ) : ( - - {todos.map((todo) => ( - - - - handleToggleTodo(todo._id, todo.completed) - } - className="mr-3" - > - - - - {todo.text} - - - handleDeleteTodo(todo._id)} - className="ml-2 p-1" - > - - - - ))} - - )} - {{else}} - {todos.isLoading ? ( - - - - ) : todos.data?.length === 0 ? ( - - No todos yet. Add one above! - - ) : ( - - {todos.data?.map((todo) => ( - - - - handleToggleTodo(todo.id, todo.completed) - } - className="mr-3" - > - - - - {todo.text} - - - handleDeleteTodo(todo.id)} - className="ml-2 p-1" - > - - - - ))} - - )} - {{/if}} - - - - - ); -} diff --git a/apps/cli/templates/examples/todo/native/uniwind/app/(drawer)/todos.tsx.hbs b/apps/cli/templates/examples/todo/native/uniwind/app/(drawer)/todos.tsx.hbs new file mode 100644 index 000000000..69566b80b --- /dev/null +++ b/apps/cli/templates/examples/todo/native/uniwind/app/(drawer)/todos.tsx.hbs @@ -0,0 +1,295 @@ +import { useState } from "react"; +import { + View, + Text, + TextInput, + ScrollView, + ActivityIndicator, + Alert, + Pressable, +} from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +{{#if (eq backend "convex")}} +import { useMutation, useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import type { Id } from "@{{projectName}}/backend/convex/_generated/dataModel"; +{{else}} +import { useMutation, useQuery } from "@tanstack/react-query"; +{{/if}} +import { Container } from "@/components/container"; +{{#unless (eq backend "convex")}} + {{#if (eq api "orpc")}} + import { orpc } from "@/utils/orpc"; + {{/if}} + {{#if (eq api "trpc")}} + import { trpc } from "@/utils/trpc"; + {{/if}} +{{/unless}} +import { Card, Checkbox, useThemeColor, Chip } from "heroui-native"; + +export default function TodosScreen() { + const [newTodoText, setNewTodoText] = useState(""); + {{#if (eq backend "convex")}} + const todos = useQuery(api.todos.getAll); + const createTodoMutation = useMutation(api.todos.create); + const toggleTodoMutation = useMutation(api.todos.toggle); + const deleteTodoMutation = useMutation(api.todos.deleteTodo); + {{else}} + {{#if (eq api "orpc")}} + const todos = useQuery(orpc.todo.getAll.queryOptions()); + const createMutation = useMutation(orpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + })); + const toggleMutation = useMutation(orpc.todo.toggle.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + })); + const deleteMutation = useMutation(orpc.todo.delete.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + })); + {{/if}} + {{#if (eq api "trpc")}} + const todos = useQuery(trpc.todo.getAll.queryOptions()); + const createMutation = useMutation(trpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + })); + const toggleMutation = useMutation(trpc.todo.toggle.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + })); + const deleteMutation = useMutation(trpc.todo.delete.mutationOptions({ + onSuccess: () => { + todos.refetch(); + }, + })); + {{/if}} + {{/if}} + + const mutedColor = useThemeColor("muted"); + const accentColor = useThemeColor("accent"); + const dangerColor = useThemeColor("danger"); + const foregroundColor = useThemeColor("foreground"); + + {{#if (eq backend "convex")}} + const handleAddTodo = async () => { + const text = newTodoText.trim(); + if (!text) return; + await createTodoMutation({ text }); + setNewTodoText(""); + }; + + const handleToggleTodo = (id: Id<"todos">, currentCompleted: boolean) => { + toggleTodoMutation({ id, completed: !currentCompleted }); + }; + + const handleDeleteTodo = (id: Id<"todos">) => { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteTodoMutation({ id }), + }, + ]); + }; + + const isLoading = !todos; + const completedCount = todos?.filter((t) => t.completed).length || 0; + const totalCount = todos?.length || 0; + {{else}} + const handleAddTodo = () => { + if (newTodoText.trim()) { + createMutation.mutate({ text: newTodoText }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + Alert.alert("Delete Todo", "Are you sure you want to delete this todo?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + style: "destructive", + onPress: () => deleteMutation.mutate({ id }), + }, + ]); + }; + + const isLoading = todos?.isLoading; + const completedCount = todos?.data?.filter((t) => t.completed).length || 0; + const totalCount = todos?.data?.length || 0; + {{/if}} + + return ( + + + + + + Todo List + + {totalCount > 0 && ( + + + {completedCount}/{totalCount} + + + )} + + + + + + + + + + {{#if (eq backend "convex")}} + + {{else}} + {createMutation.isPending ? ( + + ) : ( + + )} + {{/if}} + + + + + {{#if (eq backend "convex")}} + {isLoading && ( + + + Loading todos... + + )} + + {todos && todos.length === 0 && !isLoading && ( + + + + No todos yet + + + Add your first task to get started! + + + )} + + {todos && todos.length > 0 && ( + + {todos.map((todo) => ( + + + handleToggleTodo(todo._id, todo.completed)} + /> + + + {todo.text} + + + handleDeleteTodo(todo._id)} + className="p-2 rounded-lg active:opacity-70" + > + + + + + ))} + + )} + {{else}} + {isLoading && ( + + + Loading todos... + + )} + + {todos?.data && todos.data.length === 0 && !isLoading && ( + + + + No todos yet + + + Add your first task to get started! + + + )} + + {todos?.data && todos.data.length > 0 && ( + + {todos.data.map((todo) => ( + + + handleToggleTodo(todo.id, todo.completed)} + /> + + + {todo.text} + + + handleDeleteTodo(todo.id)} + className="p-2 rounded-lg active:opacity-70" + > + + + + + ))} + + )} + {{/if}} + + + ); +} \ No newline at end of file diff --git a/apps/cli/templates/extras/bunfig.toml.hbs b/apps/cli/templates/extras/bunfig.toml.hbs index 32357654a..8348bd193 100644 --- a/apps/cli/templates/extras/bunfig.toml.hbs +++ b/apps/cli/templates/extras/bunfig.toml.hbs @@ -1,6 +1,6 @@ [install] -{{#if (or (includes frontend "nuxt") (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}} -linker = "hoisted" # having issues with Nuxt and NativeWind/Unistyles when linker is isolated +{{#if (or (includes frontend "nuxt"))}} +linker = "hoisted" # having issues with Nuxt when linker is isolated {{else}} linker = "isolated" -{{/if}} +{{/if}} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/bare/_gitignore b/apps/cli/templates/frontend/native/bare/_gitignore new file mode 100644 index 000000000..fc7bb369b --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/_gitignore @@ -0,0 +1,18 @@ +node_modules/ +.expo/ +dist/ +npm-debug.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision +*.orig.* +web-build/ + +# macOS +.DS_Store + +# Temporary files created by Metro to check the health of the file watcher +.metro-health-check* + diff --git a/apps/cli/templates/frontend/native/nativewind/app.json.hbs b/apps/cli/templates/frontend/native/bare/app.json.hbs similarity index 99% rename from apps/cli/templates/frontend/native/nativewind/app.json.hbs rename to apps/cli/templates/frontend/native/bare/app.json.hbs index 86a6b10ab..21d0859d2 100644 --- a/apps/cli/templates/frontend/native/nativewind/app.json.hbs +++ b/apps/cli/templates/frontend/native/bare/app.json.hbs @@ -47,3 +47,4 @@ } } } + diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/_layout.tsx.hbs similarity index 61% rename from apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx.hbs rename to apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/_layout.tsx.hbs index a1a1f04ad..627082b33 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx.hbs +++ b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/_layout.tsx.hbs @@ -1,27 +1,21 @@ import { TabBarIcon } from "@/components/tabbar-icon"; import { useColorScheme } from "@/lib/use-color-scheme"; import { Tabs } from "expo-router"; +import { NAV_THEME } from "@/lib/constants"; export default function TabLayout() { const { isDarkColorScheme } = useColorScheme(); + const theme = isDarkColorScheme ? NAV_THEME.dark : NAV_THEME.light; return ( @@ -44,3 +38,4 @@ export default function TabLayout() { ); } + diff --git a/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/index.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/index.tsx.hbs new file mode 100644 index 000000000..1d55bf21c --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/index.tsx.hbs @@ -0,0 +1,43 @@ +import { Container } from "@/components/container"; +import { ScrollView, Text, View, StyleSheet } from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +export default function TabOne() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + + return ( + + + + + Tab One + + + Explore the first section of your app + + + + + ); +} + +const styles = StyleSheet.create({ + scrollView: { + flex: 1, + padding: 16, + }, + content: { + paddingVertical: 16, + }, + title: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + }, +}); + diff --git a/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/two.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/two.tsx.hbs new file mode 100644 index 000000000..0ffa6a31c --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/(drawer)/(tabs)/two.tsx.hbs @@ -0,0 +1,43 @@ +import { Container } from "@/components/container"; +import { ScrollView, Text, View, StyleSheet } from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +export default function TabTwo() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + + return ( + + + + + Tab Two + + + Discover more features and content + + + + + ); +} + +const styles = StyleSheet.create({ + scrollView: { + flex: 1, + padding: 16, + }, + content: { + paddingVertical: 16, + }, + title: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 8, + }, + subtitle: { + fontSize: 16, + }, +}); + diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/(drawer)/_layout.tsx.hbs similarity index 72% rename from apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs rename to apps/cli/templates/frontend/native/bare/app/(drawer)/_layout.tsx.hbs index a643ad063..3a77a4f5c 100644 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx.hbs +++ b/apps/cli/templates/frontend/native/bare/app/(drawer)/_layout.tsx.hbs @@ -1,12 +1,34 @@ import { Ionicons, MaterialIcons } from "@expo/vector-icons"; import { Link } from "expo-router"; import { Drawer } from "expo-router/drawer"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; import { HeaderButton } from "@/components/header-button"; const DrawerLayout = () => { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + return ( - + { }; export default DrawerLayout; + diff --git a/apps/cli/templates/frontend/native/bare/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/(drawer)/index.tsx.hbs new file mode 100644 index 000000000..8e7c1a612 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/(drawer)/index.tsx.hbs @@ -0,0 +1,234 @@ +import { View, Text, ScrollView, TouchableOpacity, StyleSheet } from "react-native"; +import { Container } from "@/components/container"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; +{{#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}} +{{#if (and (eq backend "convex") (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 if (and (eq backend "convex") (eq auth "better-auth"))}} +import { useConvexAuth, useQuery } from "convex/react"; +import { api } from "@{{ projectName }}/backend/convex/_generated/api"; +import { authClient } from "@/lib/auth-client"; +import { SignIn } from "@/components/sign-in"; +import { SignUp } from "@/components/sign-up"; +{{else if (eq backend "convex")}} +import { useQuery } from "convex/react"; +import { api } from "@{{ projectName }}/backend/convex/_generated/api"; +{{/if}} + +export default function Home() { +const { colorScheme } = useColorScheme(); +const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; +{{#if (eq api "orpc")}} +const healthCheck = useQuery(orpc.healthCheck.queryOptions()); +{{/if}} +{{#if (eq api "trpc")}} +const healthCheck = useQuery(trpc.healthCheck.queryOptions()); +{{/if}} +{{#if (and (eq backend "convex") (eq auth "clerk"))}} +const { user } = useUser(); +const healthCheck = useQuery(api.healthCheck.get); +const privateData = useQuery(api.privateData.get); +{{else if (and (eq backend "convex") (eq auth "better-auth"))}} +const healthCheck = useQuery(api.healthCheck.get); +const { isAuthenticated } = useConvexAuth(); +const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip"); +{{else if (eq backend "convex")}} +const healthCheck = useQuery(api.healthCheck.get); +{{/if}} + +return ( + + + + + BETTER T STACK + + + {{#unless (and (eq backend "convex") (eq auth "better-auth"))}} + + {{#if (eq backend "convex")}} + + + + + Convex + + + {healthCheck === undefined + ? "Checking..." + : healthCheck === "OK" + ? "Connected to API" + : "API Disconnected"} + + + + {{else}} + {{#unless (eq api "none")}} + + + + + {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} + + + {healthCheck.isLoading + ? "Checking connection..." + : healthCheck.data + ? "All systems operational" + : "Service unavailable"} + + + + {{/unless}} + {{/if}} + + {{/unless}} + + {{#if (and (eq backend "convex") (eq auth "clerk"))}} + + Hello {user?.emailAddresses[0].emailAddress} + Private Data: {privateData?.message} + + + + + Sign in + + + Sign up + + + + Loading... + + {{/if}} + + {{#if (and (eq backend "convex") (eq auth "better-auth"))}} + {user ? ( + + + + Welcome, {user.name} + + + + {user.email} + + { + authClient.signOut(); + }} + > + Sign Out + + + ) : null} + + + API Status + + + + + {healthCheck === undefined + ? "Checking..." + : healthCheck === "OK" + ? "Connected to API" + : "API Disconnected"} + + + + {!user && ( + <> + + + + )} + {{/if}} + + + +); +} + +const styles = StyleSheet.create({ +scrollView: { +flex: 1, +}, +content: { +padding: 16, +}, +title: { +fontSize: 24, +fontWeight: "bold", +marginBottom: 16, +}, +card: { +padding: 16, +marginBottom: 16, +borderWidth: 1, +}, +statusRow: { +flexDirection: "row", +alignItems: "center", +gap: 8, +}, +statusIndicator: { +height: 8, +width: 8, +}, +statusContent: { +flex: 1, +}, +statusTitle: { +fontSize: 14, +fontWeight: "bold", +}, +statusText: { +fontSize: 12, +}, +userCard: { +marginBottom: 16, +padding: 16, +borderWidth: 1, +}, +userHeader: { +marginBottom: 8, +}, +userText: { +fontSize: 16, +}, +userName: { +fontWeight: "bold", +}, +userEmail: { +fontSize: 14, +marginBottom: 12, +}, +signOutButton: { +padding: 12, +}, +signOutText: { +color: "#ffffff", +}, +statusCard: { +marginBottom: 16, +padding: 16, +borderWidth: 1, +}, +statusCardTitle: { +marginBottom: 8, +fontWeight: "bold", +}, +}); \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/bare/app/+not-found.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/+not-found.tsx.hbs new file mode 100644 index 000000000..100e1dd59 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/+not-found.tsx.hbs @@ -0,0 +1,65 @@ +import { Container } from "@/components/container"; +import { Link, Stack } from "expo-router"; +import { Text, View, StyleSheet } from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +export default function NotFoundScreen() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + + return ( + <> + + + + + 🤔 + + Page Not Found + + + Sorry, the page you're looking for doesn't exist. + + + + Go to Home + + + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + content: { + alignItems: "center", + }, + emoji: { + fontSize: 48, + marginBottom: 16, + }, + title: { + fontSize: 20, + fontWeight: "bold", + marginBottom: 8, + textAlign: "center", + }, + subtitle: { + fontSize: 14, + textAlign: "center", + marginBottom: 24, + }, + link: { + padding: 12, + }, +}); + diff --git a/apps/cli/templates/frontend/native/bare/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/_layout.tsx.hbs new file mode 100644 index 000000000..f99595437 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/_layout.tsx.hbs @@ -0,0 +1,163 @@ +{{#if (includes examples "ai")}} +import "@/polyfills"; +{{/if}} + +{{#if (eq backend "convex")}} + {{#if (eq auth "better-auth")}} + import { ConvexReactClient } from "convex/react"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; + {{else}} + import { ConvexProvider, ConvexReactClient } from "convex/react"; + {{/if}} + {{#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"; + {{/unless}} +{{/if}} + +import { Stack } from "expo-router"; +import { + DarkTheme, + DefaultTheme, + type Theme, + ThemeProvider, +} from "@react-navigation/native"; +import { StatusBar } from "expo-status-bar"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +{{#if (eq api "trpc")}} +import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} +import { queryClient } from "@/utils/orpc"; +{{/if}} +import { NAV_THEME } from "@/lib/constants"; +import React, { useRef } from "react"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { Platform, StyleSheet } from "react-native"; +import { setAndroidNavigationBar } from "@/lib/android-navigation-bar"; + +const LIGHT_THEME: Theme = { + ...DefaultTheme, + colors: NAV_THEME.light, +}; +const DARK_THEME: Theme = { + ...DarkTheme, + colors: NAV_THEME.dark, +}; + +export const unstable_settings = { + initialRouteName: "(drawer)", +}; + +{{#if (eq backend "convex")}} +const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, { + unsavedChangesWarning: false, +}); +{{/if}} + +const useIsomorphicLayoutEffect = + Platform.OS === "web" && typeof window === "undefined" + ? React.useEffect + : React.useLayoutEffect; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +export default function RootLayout() { + const hasMounted = useRef(false); + const { colorScheme, isDarkColorScheme } = useColorScheme(); + const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false); + + useIsomorphicLayoutEffect(() => { + if (hasMounted.current) { + return; + } + setAndroidNavigationBar(colorScheme); + setIsColorSchemeLoaded(true); + hasMounted.current = true; + }, []); + + if (!isColorSchemeLoaded) { + return null; + } + + return ( + <> + {{#if (eq backend "convex")}} + {{#if (eq auth "clerk")}} + + + + + + + + + + + + + + + {{else if (eq auth "better-auth")}} + + + + + + + + + + + + {{else}} + + + + + + + + + + + + {{/if}} + {{else}} + {{#unless (eq api "none")}} + + + + + + + + + + + + {{else}} + + + + + + + + + + {{/unless}} + {{/if}} + + ); +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/bare/app/modal.tsx.hbs b/apps/cli/templates/frontend/native/bare/app/modal.tsx.hbs new file mode 100644 index 000000000..e568296c8 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/app/modal.tsx.hbs @@ -0,0 +1,34 @@ +import { Container } from "@/components/container"; +import { Text, View, StyleSheet } from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +export default function Modal() { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + + return ( + + + + Modal + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 16, + }, + header: { + marginBottom: 16, + }, + title: { + fontSize: 20, + fontWeight: "bold", + }, +}); + diff --git a/apps/cli/templates/frontend/native/bare/components/container.tsx.hbs b/apps/cli/templates/frontend/native/bare/components/container.tsx.hbs new file mode 100644 index 000000000..1e491fad8 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/components/container.tsx.hbs @@ -0,0 +1,25 @@ +import React from "react"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; +import { StyleSheet } from "react-native"; + +export function Container({ children }: { children: React.ReactNode }) { + const { colorScheme } = useColorScheme(); + const backgroundColor = colorScheme === "dark" + ? NAV_THEME.dark.background + : NAV_THEME.light.background; + + return ( + + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + diff --git a/apps/cli/templates/frontend/native/bare/components/header-button.tsx.hbs b/apps/cli/templates/frontend/native/bare/components/header-button.tsx.hbs new file mode 100644 index 000000000..88f433f83 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/components/header-button.tsx.hbs @@ -0,0 +1,47 @@ +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import { forwardRef } from "react"; +import { Pressable, StyleSheet, View } from "react-native"; +import { useColorScheme } from "@/lib/use-color-scheme"; +import { NAV_THEME } from "@/lib/constants"; + +export const HeaderButton = forwardRef< + View, + { onPress?: () => void } +>(({ onPress }, ref) => { + const { colorScheme } = useColorScheme(); + const theme = colorScheme === "dark" ? NAV_THEME.dark : NAV_THEME.light; + + return ( + [ + styles.button, + { + backgroundColor: pressed + ? theme.background + : theme.card, + }, + ]} + > + {({ pressed }) => ( + + )} + + ); +}); + +const styles = StyleSheet.create({ + button: { + padding: 8, + marginRight: 8, + }, +}); + diff --git a/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx.hbs b/apps/cli/templates/frontend/native/bare/components/tabbar-icon.tsx.hbs similarity index 99% rename from apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx.hbs rename to apps/cli/templates/frontend/native/bare/components/tabbar-icon.tsx.hbs index ecf1944c1..4f83b65e2 100644 --- a/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx.hbs +++ b/apps/cli/templates/frontend/native/bare/components/tabbar-icon.tsx.hbs @@ -6,3 +6,4 @@ export const TabBarIcon = (props: { }) => { return ; }; + diff --git a/apps/cli/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx.hbs b/apps/cli/templates/frontend/native/bare/lib/android-navigation-bar.tsx.hbs similarity index 99% rename from apps/cli/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx.hbs rename to apps/cli/templates/frontend/native/bare/lib/android-navigation-bar.tsx.hbs index 47d6134d8..c7861506c 100644 --- a/apps/cli/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx.hbs +++ b/apps/cli/templates/frontend/native/bare/lib/android-navigation-bar.tsx.hbs @@ -9,3 +9,4 @@ export async function setAndroidNavigationBar(theme: "light" | "dark") { theme === "dark" ? NAV_THEME.dark.background : NAV_THEME.light.background, ); } + diff --git a/apps/cli/templates/frontend/native/nativewind/lib/constants.ts.hbs b/apps/cli/templates/frontend/native/bare/lib/constants.ts.hbs similarity index 99% rename from apps/cli/templates/frontend/native/nativewind/lib/constants.ts.hbs rename to apps/cli/templates/frontend/native/bare/lib/constants.ts.hbs index 8e7ca1180..73792a692 100644 --- a/apps/cli/templates/frontend/native/nativewind/lib/constants.ts.hbs +++ b/apps/cli/templates/frontend/native/bare/lib/constants.ts.hbs @@ -16,3 +16,4 @@ export const NAV_THEME = { text: "hsl(210 40% 98%)", }, }; + diff --git a/apps/cli/templates/frontend/native/bare/lib/use-color-scheme.ts.hbs b/apps/cli/templates/frontend/native/bare/lib/use-color-scheme.ts.hbs new file mode 100644 index 000000000..c5b14a900 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/lib/use-color-scheme.ts.hbs @@ -0,0 +1,20 @@ +import { useColorScheme as useRNColorScheme } from "react-native"; + +export function useColorScheme() { + const systemColorScheme = useRNColorScheme(); + const colorScheme = systemColorScheme ?? "light"; + + return { + colorScheme: colorScheme as "light" | "dark", + isDarkColorScheme: colorScheme === "dark", + setColorScheme: () => { + // Color scheme is managed by the system in bare mode + console.warn("setColorScheme is not available in bare mode. Color scheme is managed by the system."); + }, + toggleColorScheme: () => { + // Color scheme is managed by the system in bare mode + console.warn("toggleColorScheme is not available in bare mode. Color scheme is managed by the system."); + }, + }; +} + diff --git a/apps/cli/templates/frontend/native/bare/metro.config.js.hbs b/apps/cli/templates/frontend/native/bare/metro.config.js.hbs new file mode 100644 index 000000000..81e7c2c7b --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/metro.config.js.hbs @@ -0,0 +1,9 @@ +// Learn more https://docs.expo.io/guides/customizing-metro +const { getDefaultConfig } = require("expo/metro-config"); + +const config = getDefaultConfig(__dirname); + +config.resolver.unstable_enablePackageExports = true; + +module.exports = config; + diff --git a/apps/cli/templates/frontend/native/nativewind/package.json.hbs b/apps/cli/templates/frontend/native/bare/package.json.hbs similarity index 96% rename from apps/cli/templates/frontend/native/nativewind/package.json.hbs rename to apps/cli/templates/frontend/native/bare/package.json.hbs index ad3c76773..b70273f83 100644 --- a/apps/cli/templates/frontend/native/nativewind/package.json.hbs +++ b/apps/cli/templates/frontend/native/bare/package.json.hbs @@ -31,7 +31,6 @@ "expo-status-bar": "~3.0.7", "expo-system-ui": "~6.0.7", "expo-web-browser": "~15.0.6", - "nativewind": "^4.1.23", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.4", @@ -45,8 +44,8 @@ "devDependencies": { "@babel/core": "^7.26.10", "@types/react": "~19.1.10", - "tailwindcss": "^3.4.17", "typescript": "~5.8.2" }, "private": true } + diff --git a/apps/cli/templates/frontend/native/bare/tsconfig.json.hbs b/apps/cli/templates/frontend/native/bare/tsconfig.json.hbs new file mode 100644 index 000000000..5fdd74bf7 --- /dev/null +++ b/apps/cli/templates/frontend/native/bare/tsconfig.json.hbs @@ -0,0 +1,11 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"] +} + diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx.hbs deleted file mode 100644 index 24a051215..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx.hbs +++ /dev/null @@ -1,19 +0,0 @@ -import { Container } from "@/components/container"; -import { ScrollView, Text, View } from "react-native"; - -export default function TabOne() { - return ( - - - - - Tab One - - - Explore the first section of your app - - - - - ); -} diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx.hbs deleted file mode 100644 index 1736606a3..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx.hbs +++ /dev/null @@ -1,19 +0,0 @@ -import { Container } from "@/components/container"; -import { ScrollView, Text, View } from "react-native"; - -export default function TabTwo() { - return ( - - - - - Tab Two - - - Discover more features and content - - - - - ); -} diff --git a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs deleted file mode 100644 index 99ca6ca2e..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs +++ /dev/null @@ -1,178 +0,0 @@ -import { View, Text, ScrollView, TouchableOpacity } from "react-native"; -import { Container } from "@/components/container"; -{{#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}} -{{#if (and (eq backend "convex") (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 if (and (eq backend "convex") (eq auth "better-auth"))}} -import { useConvexAuth, useQuery } from "convex/react"; -import { api } from "@{{ projectName }}/backend/convex/_generated/api"; -import { authClient } from "@/lib/auth-client"; -import { SignIn } from "@/components/sign-in"; -import { SignUp } from "@/components/sign-up"; -{{else if (eq backend "convex")}} -import { useQuery } from "convex/react"; -import { api } from "@{{ projectName }}/backend/convex/_generated/api"; -{{/if}} - -export default function Home() { - {{#if (eq api "orpc")}} - const healthCheck = useQuery(orpc.healthCheck.queryOptions()); - {{/if}} - {{#if (eq api "trpc")}} - const healthCheck = useQuery(trpc.healthCheck.queryOptions()); - {{/if}} - {{#if (and (eq backend "convex") (eq auth "clerk"))}} - const { user } = useUser(); - const healthCheck = useQuery(api.healthCheck.get); - const privateData = useQuery(api.privateData.get); - {{else if (and (eq backend "convex") (eq auth "better-auth"))}} - const healthCheck = useQuery(api.healthCheck.get); - const { isAuthenticated } = useConvexAuth(); - const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip"); - {{else if (eq backend "convex")}} - const healthCheck = useQuery(api.healthCheck.get); - {{/if}} - - return ( - - - - - BETTER T STACK - - - {{#unless (and (eq backend "convex") (eq auth "better-auth"))}} - - {{#if (eq backend "convex")}} - - - - - Convex - - - { - healthCheck === undefined - ? "Checking..." - : healthCheck === "OK" - ? "Connected to API" - : "API Disconnected" - } - - - - {{else}} - {{#unless (eq api "none")}} - - - - - {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} - - - {healthCheck.isLoading - ? "Checking connection..." - : healthCheck.data - ? "All systems operational" - : "Service unavailable"} - - - - {{/unless}} - {{/if}} - - {{/unless}} - - {{#if (and (eq backend "convex") (eq auth "clerk"))}} - - Hello {user?.emailAddresses[0].emailAddress} - Private Data: {privateData?.message} - - - - - Sign in - - - Sign up - - - - Loading... - - {{/if}} - - {{#if (and (eq backend "convex") (eq auth "better-auth"))}} - {user ? ( - - - - Welcome,{" "} - {user.name} - - - - {user.email} - - { - authClient.signOut(); - }} - > - Sign Out - - - ) : null} - - - API Status - - - - - { - healthCheck === undefined - ? "Checking..." - : healthCheck === "OK" - ? "Connected to API" - : "API Disconnected" - } - - - - {!user && ( - <> - - - - )} - {{/if}} - - - - ); -} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx.hbs deleted file mode 100644 index 8a9f34000..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx.hbs +++ /dev/null @@ -1,29 +0,0 @@ -import { Container } from "@/components/container"; -import { Link, Stack } from "expo-router"; -import { Text, View } from "react-native"; - -export default function NotFoundScreen() { - return ( - <> - - - - - 🤔 - - Page Not Found - - - Sorry, the page you're looking for doesn't exist. - - - - Go to Home - - - - - - - ); -} diff --git a/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs deleted file mode 100644 index 64ac0ccbe..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs +++ /dev/null @@ -1,175 +0,0 @@ -{{#if (includes examples "ai")}} -import "@/polyfills"; -{{/if}} -{{#if (eq backend "convex")}} -{{#if (eq auth "better-auth")}} -import { ConvexReactClient } from "convex/react"; -import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; -import { authClient } from "@/lib/auth-client"; -{{else}} -import { ConvexProvider, ConvexReactClient } from "convex/react"; -{{/if}} -{{#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"; -{{/unless}} -{{/if}} -import { Stack } from "expo-router"; -import { - DarkTheme, - DefaultTheme, - type Theme, - ThemeProvider, -} from "@react-navigation/native"; -import { StatusBar } from "expo-status-bar"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import "../global.css"; -{{#if (eq api "trpc")}} -import { queryClient } from "@/utils/trpc"; -{{/if}} -{{#if (eq api "orpc")}} -import { queryClient } from "@/utils/orpc"; -{{/if}} -import { NAV_THEME } from "@/lib/constants"; -import React, { useRef } from "react"; -import { useColorScheme } from "@/lib/use-color-scheme"; -import { Platform } from "react-native"; -import { setAndroidNavigationBar } from "@/lib/android-navigation-bar"; - -const LIGHT_THEME: Theme = { - ...DefaultTheme, - colors: NAV_THEME.light, -}; -const DARK_THEME: Theme = { - ...DarkTheme, - colors: NAV_THEME.dark, -}; - -export const unstable_settings = { - initialRouteName: "(drawer)", -}; - -{{#if (eq backend "convex")}} -const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, { - unsavedChangesWarning: false, -}); -{{/if}} - -export default function RootLayout() { - const hasMounted = useRef(false); - const { colorScheme, isDarkColorScheme } = useColorScheme(); - const [isColorSchemeLoaded, setIsColorSchemeLoaded] = React.useState(false); - - useIsomorphicLayoutEffect(() => { - if (hasMounted.current) { - return; - } - - if (Platform.OS === "web") { - document.documentElement.classList.add("bg-background"); - } - setAndroidNavigationBar(colorScheme); - setIsColorSchemeLoaded(true); - hasMounted.current = true; - }, []); - - if (!isColorSchemeLoaded) { - return null; - } - return ( - {{#if (eq backend "convex")}} - {{#if (eq auth "clerk")}} - - - - - - - - - - - - - - - {{else if (eq auth "better-auth")}} - - - - - - - - - - - - {{else}} - - - - - - - - - - - - {{/if}} - {{else}} - {{#unless (eq api "none")}} - - - - - - - - - - - - {{else}} - - - - - - - - - - {{/unless}} - {{/if}} - ); -} - -const useIsomorphicLayoutEffect = - Platform.OS === "web" && typeof window === "undefined" - ? React.useEffect - : React.useLayoutEffect; diff --git a/apps/cli/templates/frontend/native/nativewind/app/modal.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/modal.tsx.hbs deleted file mode 100644 index 7fdcaea8b..000000000 --- a/apps/cli/templates/frontend/native/nativewind/app/modal.tsx.hbs +++ /dev/null @@ -1,14 +0,0 @@ -import { Container } from "@/components/container"; -import { Text, View } from "react-native"; - -export default function Modal() { - return ( - - - - Modal - - - - ); -} diff --git a/apps/cli/templates/frontend/native/nativewind/babel.config.js.hbs b/apps/cli/templates/frontend/native/nativewind/babel.config.js.hbs deleted file mode 100644 index d54d5e71b..000000000 --- a/apps/cli/templates/frontend/native/nativewind/babel.config.js.hbs +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = (api) => { - api.cache(true); - const plugins = []; - - plugins.push("react-native-worklets/plugin"); - - return { - presets: [ - ["babel-preset-expo", { jsxImportSource: "nativewind" }], - "nativewind/babel", - ], - plugins, - }; -}; diff --git a/apps/cli/templates/frontend/native/nativewind/components/container.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/components/container.tsx.hbs deleted file mode 100644 index bf824f34c..000000000 --- a/apps/cli/templates/frontend/native/nativewind/components/container.tsx.hbs +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import { SafeAreaView } from "react-native-safe-area-context"; - -export const Container = ({ children }: { children: React.ReactNode }) => { - return ( - {children} - ); -}; diff --git a/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx.hbs deleted file mode 100644 index bb3a21ad9..000000000 --- a/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx.hbs +++ /dev/null @@ -1,26 +0,0 @@ -import FontAwesome from "@expo/vector-icons/FontAwesome"; -import { forwardRef } from "react"; -import { Pressable } from "react-native"; - -export const HeaderButton = forwardRef< - typeof Pressable, - { onPress?: () => void } ->(({ onPress }, ref) => { - return ( - - {({ pressed }) => ( - - )} - - ); -}); diff --git a/apps/cli/templates/frontend/native/nativewind/global.css b/apps/cli/templates/frontend/native/nativewind/global.css deleted file mode 100644 index f658ca1d8..000000000 --- a/apps/cli/templates/frontend/native/nativewind/global.css +++ /dev/null @@ -1,50 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96%; - --secondary-foreground: 222.2 84% 4.9%; - --muted: 210 40% 96%; - --muted-foreground: 215.4 16.3% 40%; - --accent: 210 40% 96%; - --accent-foreground: 222.2 84% 4.9%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --radius: 8px; - } - - .dark:root { - --background: 222.2 84% 4.9%; - --foreground: 210 40% 98%; - --card: 222.2 84% 4.9%; - --card-foreground: 210 40% 98%; - --popover: 222.2 84% 4.9%; - --popover-foreground: 210 40% 98%; - --primary: 217.2 91.2% 59.8%; - --primary-foreground: 222.2 84% 4.9%; - --secondary: 217.2 32.6% 17.5%; - --secondary-foreground: 210 40% 98%; - --muted: 217.2 32.6% 17.5%; - --muted-foreground: 215 20.2% 70%; - --accent: 217.2 32.6% 17.5%; - --accent-foreground: 210 40% 98%; - --destructive: 0 72% 51%; - --destructive-foreground: 210 40% 98%; - --border: 217.2 32.6% 17.5%; - --input: 217.2 32.6% 17.5%; - --ring: 224.3 76.3% 94.1%; - } -} diff --git a/apps/cli/templates/frontend/native/nativewind/lib/use-color-scheme.ts.hbs b/apps/cli/templates/frontend/native/nativewind/lib/use-color-scheme.ts.hbs deleted file mode 100644 index df75499f1..000000000 --- a/apps/cli/templates/frontend/native/nativewind/lib/use-color-scheme.ts.hbs +++ /dev/null @@ -1,12 +0,0 @@ -import { useColorScheme as useNativewindColorScheme } from "nativewind"; - -export function useColorScheme() { - const { colorScheme, setColorScheme, toggleColorScheme } = - useNativewindColorScheme(); - return { - colorScheme: colorScheme ?? "dark", - isDarkColorScheme: colorScheme === "dark", - setColorScheme, - toggleColorScheme, - }; -} diff --git a/apps/cli/templates/frontend/native/nativewind/metro.config.js.hbs b/apps/cli/templates/frontend/native/nativewind/metro.config.js.hbs deleted file mode 100644 index 55fe4a81a..000000000 --- a/apps/cli/templates/frontend/native/nativewind/metro.config.js.hbs +++ /dev/null @@ -1,12 +0,0 @@ -// Learn more https://docs.expo.io/guides/customizing-metro -const { getDefaultConfig } = require("expo/metro-config"); -const { withNativeWind } = require("nativewind/metro"); - -const config = withNativeWind(getDefaultConfig(__dirname), { - input: "./global.css", - configPath: "./tailwind.config.js", -}); - -config.resolver.unstable_enablePackageExports = true; - -module.exports = config; \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/nativewind/tailwind.config.js.hbs b/apps/cli/templates/frontend/native/nativewind/tailwind.config.js.hbs deleted file mode 100644 index b6ea71147..000000000 --- a/apps/cli/templates/frontend/native/nativewind/tailwind.config.js.hbs +++ /dev/null @@ -1,59 +0,0 @@ -import { hairlineWidth } from "nativewind/theme"; - -/** @type {import('tailwindcss').Config} */ -export const darkMode = "class"; -export const content = [ - "./app/**/*.{js,ts,tsx}", - "./components/**/*.{js,ts,tsx}", -]; -export const presets = [require("nativewind/preset")]; -export const theme = { - extend: { - colors: { - background: "hsl(var(--background))", - foreground: "hsl(var(--foreground))", - card: { - DEFAULT: "hsl(var(--card))", - foreground: "hsl(var(--card-foreground))", - }, - popover: { - DEFAULT: "hsl(var(--popover))", - foreground: "hsl(var(--popover-foreground))", - }, - primary: { - DEFAULT: "hsl(var(--primary))", - foreground: "hsl(var(--primary-foreground))", - }, - secondary: { - DEFAULT: "hsl(var(--secondary))", - foreground: "hsl(var(--secondary-foreground))", - }, - muted: { - DEFAULT: "hsl(var(--muted))", - foreground: "hsl(var(--muted-foreground))", - }, - accent: { - DEFAULT: "hsl(var(--accent))", - foreground: "hsl(var(--accent-foreground))", - }, - destructive: { - DEFAULT: "hsl(var(--destructive))", - foreground: "hsl(var(--destructive-foreground))", - }, - border: "hsl(var(--border))", - input: "hsl(var(--input))", - ring: "hsl(var(--ring))", - radius: "var(--radius)", - }, - borderRadius: { - xl: "calc(var(--radius) + 4px)", - lg: "var(--radius)", - md: "calc(var(--radius) - 2px)", - sm: "calc(var(--radius) - 4px)", - }, - borderWidth: { - hairline: hairlineWidth(), - }, - }, -}; -export const plugins = []; diff --git a/apps/cli/templates/frontend/native/nativewind/_gitignore b/apps/cli/templates/frontend/native/uniwind/_gitignore similarity index 80% rename from apps/cli/templates/frontend/native/nativewind/_gitignore rename to apps/cli/templates/frontend/native/uniwind/_gitignore index 829dfca22..b1b034d82 100644 --- a/apps/cli/templates/frontend/native/nativewind/_gitignore +++ b/apps/cli/templates/frontend/native/uniwind/_gitignore @@ -9,17 +9,13 @@ npm-debug.* *.mobileprovision *.orig.* web-build/ -# expo router -expo-env.d.ts - -.env -.cache - -ios -android # macOS .DS_Store # Temporary files created by Metro to check the health of the file watcher .metro-health-check* + +# UniWind generated types +uniwind-types.d.ts + diff --git a/apps/cli/templates/frontend/native/uniwind/app.json.hbs b/apps/cli/templates/frontend/native/uniwind/app.json.hbs new file mode 100644 index 000000000..a54d40243 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app.json.hbs @@ -0,0 +1,19 @@ +{ + "expo": { + "scheme": "{{projectName}}", + "userInterfaceStyle": "automatic", + "orientation": "default", + "web": { + "bundler": "metro" + }, + "name": "{{projectName}}", + "slug": "{{projectName}}", + "plugins": [ + "expo-font" + ], + "experiments": { + "typedRoutes": true, + "reactCompiler": true + } + } +} diff --git a/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/_layout.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/_layout.tsx.hbs new file mode 100644 index 000000000..3c6918435 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/_layout.tsx.hbs @@ -0,0 +1,46 @@ +import { Tabs } from "expo-router"; +import { Ionicons } from "@expo/vector-icons"; +import { useThemeColor } from "heroui-native"; + +export default function TabLayout() { + const themeColorForeground = useThemeColor("foreground"); + const themeColorBackground = useThemeColor("background"); + + return ( + + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); +} diff --git a/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/index.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/index.tsx.hbs new file mode 100644 index 000000000..e092affe3 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/index.tsx.hbs @@ -0,0 +1,15 @@ +import { Container } from "@/components/container"; +import { Text, View } from "react-native"; +import { Card } from "heroui-native"; + +export default function Home() { + return ( + + + + Tab One + + + + ); +} diff --git a/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/two.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/two.tsx.hbs new file mode 100644 index 000000000..01bc323f1 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/(tabs)/two.tsx.hbs @@ -0,0 +1,15 @@ +import { Container } from "@/components/container"; +import { Text, View } from "react-native"; +import { Card } from "heroui-native"; + +export default function TabTwo() { + return ( + + + + TabTwo + + + + ); +} diff --git a/apps/cli/templates/frontend/native/uniwind/app/(drawer)/_layout.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/_layout.tsx.hbs new file mode 100644 index 000000000..2a764bad1 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/_layout.tsx.hbs @@ -0,0 +1,83 @@ +import React, { useCallback } from "react"; +import { Ionicons, MaterialIcons } from "@expo/vector-icons"; +import { Link } from "expo-router"; +import { Drawer } from "expo-router/drawer"; +import { useThemeColor } from "heroui-native"; +import { Pressable } from "react-native"; +import { ThemeToggle } from "@/components/theme-toggle"; + +function DrawerLayout() { + const themeColorForeground = useThemeColor("foreground"); + const themeColorBackground = useThemeColor("background"); + + const renderThemeToggle = useCallback(() => , []); + + return ( + + ( + + ), + }} + /> + ( + + ), + headerRight: () => ( + + + + + + ), + }} + /> + {{#if (includes examples "todo")}} + ( + + ), + }} + /> + {{/if}} + {{#if (includes examples "ai")}} + ( + + ), + }} + /> + {{/if}} + + ); +} + +export default DrawerLayout; \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/uniwind/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/index.tsx.hbs new file mode 100644 index 000000000..ac5ed0411 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/(drawer)/index.tsx.hbs @@ -0,0 +1,151 @@ +import { Text, View } from "react-native"; +import { Container } from "@/components/container"; +{{#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}} +{{#if (and (eq backend "convex") (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 if (and (eq backend "convex") (eq auth "better-auth"))}} +import { useConvexAuth, useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +import { authClient } from "@/lib/auth-client"; +import { SignIn } from "@/components/sign-in"; +import { SignUp } from "@/components/sign-up"; +{{else if (eq backend "convex")}} +import { useQuery } from "convex/react"; +import { api } from "@{{projectName}}/backend/convex/_generated/api"; +{{/if}} +import { Ionicons } from "@expo/vector-icons"; +import { Card, Chip, useThemeColor } from "heroui-native"; + +export default function Home() { +{{#if (eq api "orpc")}} + const healthCheck = useQuery(orpc.healthCheck.queryOptions()); +{{/if}} +{{#if (eq api "trpc")}} + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); +{{/if}} +{{#if (and (eq backend "convex") (eq auth "clerk"))}} + const { user } = useUser(); + const healthCheck = useQuery(api.healthCheck.get); + const privateData = useQuery(api.privateData.get); +{{else if (and (eq backend "convex") (eq auth "better-auth"))}} + const healthCheck = useQuery(api.healthCheck.get); + const { isAuthenticated } = useConvexAuth(); + const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip"); +{{else if (eq backend "convex")}} + const healthCheck = useQuery(api.healthCheck.get); +{{/if}} + const mutedColor = useThemeColor("muted"); + const successColor = useThemeColor("success"); + const dangerColor = useThemeColor("danger"); + +{{#if (eq backend "convex")}} + const isConnected = healthCheck === "OK"; + const isLoading = healthCheck === undefined; +{{else}} +{{#unless (eq api "none")}} + const isConnected = healthCheck?.data === "OK"; + const isLoading = healthCheck?.isLoading; +{{/unless}} +{{/if}} + + return ( + + + + BETTER T STACK + + + + {{#unless (and (eq backend "convex") (eq auth "better-auth"))}} + + + System Status + + + {isConnected ? "LIVE" : "OFFLINE"} + + + + + + + + + {{#if (eq backend "convex")}} + Convex Backend + {{else}} + {{#unless (eq api "none")}} + {{#if (eq api "orpc")}}ORPC{{else}}TRPC{{/if}} Backend + {{/unless}} + {{/if}} + + + {isLoading + ? "Checking connection..." + : isConnected + ? "Connected to API" + : "API Disconnected"} + + + {isLoading && ( + + )} + {!isLoading && isConnected && ( + + )} + {!isLoading && !isConnected && ( + + )} + + + + {{/unless}} + + {{#if (and (eq backend "convex") (eq auth "clerk"))}} + + + + Hello {user?.emailAddresses[0].emailAddress} + + + Private Data: {privateData?.message} + + + + + + + + Sign in + + + Sign up + + + + + Loading... + + {{/if}} + + ); +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/uniwind/app/+not-found.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/+not-found.tsx.hbs new file mode 100644 index 000000000..3817db34e --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/+not-found.tsx.hbs @@ -0,0 +1,32 @@ +import { Container } from "@/components/container"; +import { Link, Stack } from "expo-router"; +import { Text, View, Pressable } from "react-native"; +import { Card } from "heroui-native"; + +export default function NotFoundScreen() { + return ( + <> + + + + + 🤔 + + Page Not Found + + + Sorry, the page you're looking for doesn't exist. + + + + + Go to Home + + + + + + + + ); +} diff --git a/apps/cli/templates/frontend/native/uniwind/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/_layout.tsx.hbs new file mode 100644 index 000000000..79dd278a0 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/_layout.tsx.hbs @@ -0,0 +1,131 @@ +{{#if (includes examples "ai")}} +import "@/polyfills"; +{{/if}} + +import "@/global.css"; + +{{#if (eq backend "convex")}} + {{#if (eq auth "better-auth")}} + import { ConvexReactClient } from "convex/react"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; + {{else}} + import { ConvexProvider, ConvexReactClient } from "convex/react"; + {{/if}} + + {{#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"; + {{/unless}} +{{/if}} + +import { Stack } from "expo-router"; +import { HeroUINativeProvider } from "heroui-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { KeyboardProvider } from "react-native-keyboard-controller"; +import { AppThemeProvider } from "@/contexts/app-theme-context"; + +{{#if (eq api "trpc")}} + import { queryClient } from "@/utils/trpc"; +{{/if}} +{{#if (eq api "orpc")}} + import { queryClient } from "@/utils/orpc"; +{{/if}} + +export const unstable_settings = { + initialRouteName: "(drawer)", +}; + +{{#if (eq backend "convex")}} + const convexUrl = process.env.EXPO_PUBLIC_CONVEX_URL || ""; + const convex = new ConvexReactClient(convexUrl, { + unsavedChangesWarning: false, + }); +{{/if}} + +function StackLayout() { + return ( + + + {{#if (eq auth "clerk")}} + + {{/if}} + + + ); +} + +export default function Layout() { + return ( + {{#if (eq backend "convex")}} + {{#if (eq auth "clerk")}} + + + + + + + + + + + + + + {{else if (eq auth "better-auth")}} + + + + + + + + + + + + {{else}} + + + + + + + + + + + + {{/if}} + {{else}} + {{#unless (eq api "none")}} + + + + + + + + + + + + {{else}} + + + + + + + + + + {{/unless}} + {{/if}} + ); +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/uniwind/app/modal.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/app/modal.tsx.hbs new file mode 100644 index 000000000..d2d0c31f1 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/app/modal.tsx.hbs @@ -0,0 +1,53 @@ +import { Container } from "@/components/container"; +import { Text, View, Pressable } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { router } from "expo-router"; +import { Card, useThemeColor } from "heroui-native"; + +function Modal() { + const accentForegroundColor = useThemeColor("accent-foreground"); + + function handleClose() { + router.back(); + } + + return ( + + + + + + + + + Modal Screen + + + This is an example modal screen. You can use this pattern for + dialogs, confirmations, or any overlay content. + + + + + + + Close Modal + + + + + + + + + ); +} + +export default Modal; diff --git a/apps/cli/templates/frontend/native/uniwind/components/container.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/components/container.tsx.hbs new file mode 100644 index 000000000..b324c66d9 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/components/container.tsx.hbs @@ -0,0 +1,33 @@ +import { cn } from "heroui-native"; +import { type PropsWithChildren } from "react"; +import { ScrollView, View, type ViewProps } from "react-native"; +import Animated, { type AnimatedProps } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +const AnimatedView = Animated.createAnimatedComponent(View); + +type Props = AnimatedProps & { + className?: string; +}; + +export function Container({ + children, + className, + ...props +}: PropsWithChildren) { + const insets = useSafeAreaInsets(); + + return ( + + + {children} + + + ); +} diff --git a/apps/cli/templates/frontend/native/uniwind/components/theme-toggle.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/components/theme-toggle.tsx.hbs new file mode 100644 index 000000000..2ab0b21a6 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/components/theme-toggle.tsx.hbs @@ -0,0 +1,35 @@ +import { Ionicons } from '@expo/vector-icons'; +import * as Haptics from 'expo-haptics'; +import { Platform, Pressable } from 'react-native'; +import Animated, { FadeOut, ZoomIn } from 'react-native-reanimated'; +import { withUniwind } from 'uniwind'; +import { useAppTheme } from '@/contexts/app-theme-context'; + +const StyledIonicons = withUniwind(Ionicons); + +export function ThemeToggle() { + const { toggleTheme, isLight } = useAppTheme(); + + return ( + { + if (Platform.OS === 'ios') { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + toggleTheme(); + }} + className="px-2.5" + > + {isLight ? ( + + + + ) : ( + + + + )} + + ); +} + diff --git a/apps/cli/templates/frontend/native/uniwind/contexts/app-theme-context.tsx.hbs b/apps/cli/templates/frontend/native/uniwind/contexts/app-theme-context.tsx.hbs new file mode 100644 index 000000000..1cd958b06 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/contexts/app-theme-context.tsx.hbs @@ -0,0 +1,62 @@ +import React, { createContext, useCallback, useContext, useMemo } from 'react'; +import { Uniwind, useUniwind } from 'uniwind'; + +type ThemeName = 'light' | 'dark'; + +type AppThemeContextType = { + currentTheme: string; + isLight: boolean; + isDark: boolean; + setTheme: (theme: ThemeName) => void; + toggleTheme: () => void; +} + +const AppThemeContext = createContext( + undefined +); + +export const AppThemeProvider = ({ children }: { children: React.ReactNode }) => { + const { theme } = useUniwind(); + + const isLight = useMemo(() => { + return theme === 'light'; + }, [theme]); + + const isDark = useMemo(() => { + return theme === 'dark'; + }, [theme]); + + const setTheme = useCallback((newTheme: ThemeName) => { + Uniwind.setTheme(newTheme); + }, []); + + const toggleTheme = useCallback(() => { + Uniwind.setTheme(theme === 'light' ? 'dark' : 'light'); + }, [theme]); + + const value = useMemo( + () => ({ + currentTheme: theme, + isLight, + isDark, + setTheme, + toggleTheme, + }), + [theme, isLight, isDark, setTheme, toggleTheme] + ); + + return ( + + {children} + + ); +}; + +export function useAppTheme() { + const context = useContext(AppThemeContext); + if (!context) { + throw new Error('useAppTheme must be used within AppThemeProvider'); + } + return context; +} + diff --git a/apps/cli/templates/frontend/native/uniwind/global.css b/apps/cli/templates/frontend/native/uniwind/global.css new file mode 100644 index 000000000..3fb7bc04b --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/global.css @@ -0,0 +1,5 @@ +@import 'tailwindcss'; +@import 'uniwind'; +@import 'heroui-native/styles'; + +@source './node_modules/heroui-native/lib'; \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/uniwind/metro.config.js.hbs b/apps/cli/templates/frontend/native/uniwind/metro.config.js.hbs new file mode 100644 index 000000000..016ed2562 --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/metro.config.js.hbs @@ -0,0 +1,13 @@ +const { getDefaultConfig } = require("expo/metro-config"); +const { withUniwindConfig } = require("uniwind/metro"); + +/** @type {import('expo/metro-config').MetroConfig} */ +const config = getDefaultConfig(__dirname); + +const uniwindConfig = withUniwindConfig(config, { + cssEntryFile: "./global.css", + dtsFile: "./uniwind-types.d.ts", +}); + +module.exports = uniwindConfig; + diff --git a/apps/cli/templates/frontend/native/uniwind/package.json.hbs b/apps/cli/templates/frontend/native/uniwind/package.json.hbs new file mode 100644 index 000000000..b4127d0fa --- /dev/null +++ b/apps/cli/templates/frontend/native/uniwind/package.json.hbs @@ -0,0 +1,54 @@ +{ + "name": "native", + "version": "1.0.0", + "main": "expo-router/entry", + "scripts": { + "start": "expo start", + "dev": "expo start --clear", + "android": "expo run:android", + "ios": "expo run:ios", + "prebuild": "expo prebuild", + "web": "expo start --web" + }, + "dependencies": { + "@expo/metro-runtime": "~6.1.2", + "@expo/vector-icons": "^15.0.3", + "@gorhom/bottom-sheet": "^5", + "@react-navigation/drawer": "^7.3.9", + "@react-navigation/elements": "^2.8.1", + {{#if (includes examples "ai")}} + "@stardazed/streams-text-encoding": "^1.0.2", + "@ungap/structured-clone": "^1.3.0", + {{/if}} + "expo": "^54.0.23", + "expo-constants": "~18.0.10", + "expo-font": "~14.0.9", + "expo-haptics": "^15.0.7", + "expo-linking": "~8.0.8", + "expo-router": "~6.0.14", + "expo-secure-store": "~15.0.7", + "expo-status-bar": "~3.0.8", + "heroui-native": "^1.0.0-beta.1", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-native": "0.81.5", + "react-native-gesture-handler": "~2.28.0", + "react-native-keyboard-controller": "1.18.5", + "react-native-reanimated": "~4.1.0", + "react-native-safe-area-context": "5.6.0", + "react-native-screens": "~4.16.0", + "react-native-svg": "15.12.1", + "react-native-web": "^0.21.0", + "react-native-worklets": "0.5.1", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^3.1.0", + "tailwindcss": "~4.1.16", + "uniwind": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^24.10.0", + "@types/react": "~19.1.0", + "typescript": "~5.9.2" + }, + "private": true +} \ No newline at end of file diff --git a/apps/cli/templates/frontend/native/nativewind/tsconfig.json.hbs b/apps/cli/templates/frontend/native/uniwind/tsconfig.json.hbs similarity index 52% rename from apps/cli/templates/frontend/native/nativewind/tsconfig.json.hbs rename to apps/cli/templates/frontend/native/uniwind/tsconfig.json.hbs index bf74d1d7b..faf0b6d64 100644 --- a/apps/cli/templates/frontend/native/nativewind/tsconfig.json.hbs +++ b/apps/cli/templates/frontend/native/uniwind/tsconfig.json.hbs @@ -2,17 +2,13 @@ "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, + "baseUrl": ".", "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ "**/*.ts", - "**/*.tsx", - ".expo/types/**/*.ts", - "expo-env.d.ts", - "nativewind-env.d.ts" + "**/*.tsx" ] -} +} \ No newline at end of file diff --git a/apps/cli/test/addons.test.ts b/apps/cli/test/addons.test.ts index faa61dee7..b9e087518 100644 --- a/apps/cli/test/addons.test.ts +++ b/apps/cli/test/addons.test.ts @@ -77,7 +77,8 @@ describe("Addon Configurations", () => { const pwaIncompatibleFrontends = [ "nuxt", "svelte", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; @@ -154,7 +155,8 @@ describe("Addon Configurations", () => { } const tauriIncompatibleFrontends = [ - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; diff --git a/apps/cli/test/api.test.ts b/apps/cli/test/api.test.ts index 360da679f..385098f4b 100644 --- a/apps/cli/test/api.test.ts +++ b/apps/cli/test/api.test.ts @@ -49,7 +49,11 @@ describe("API Configurations", () => { }); it("should work with tRPC + native frontends", async () => { - const nativeFrontends = ["native-nativewind", "native-unistyles"]; + const nativeFrontends = [ + "native-bare", + "native-uniwind", + "native-unistyles", + ]; for (const frontend of nativeFrontends) { const result = await runTRPCTest({ @@ -181,7 +185,8 @@ describe("API Configurations", () => { "nuxt", "svelte", "solid", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; @@ -565,7 +570,7 @@ describe("API Configurations", () => { const result = await runTRPCTest({ projectName: "api-complex-frontend", api: "trpc", - frontend: ["tanstack-router", "native-nativewind"], // Web + Native + frontend: ["tanstack-router", "native-bare"], // Web + Native backend: "hono", runtime: "bun", database: "sqlite", diff --git a/apps/cli/test/auth.test.ts b/apps/cli/test/auth.test.ts index 95d5f2d11..656966ea1 100644 --- a/apps/cli/test/auth.test.ts +++ b/apps/cli/test/auth.test.ts @@ -127,7 +127,8 @@ describe("Authentication Configurations", () => { "nuxt", "svelte", "solid", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; @@ -213,7 +214,8 @@ describe("Authentication Configurations", () => { "react-router", "tanstack-start", "next", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", ]; @@ -455,7 +457,7 @@ describe("Authentication Configurations", () => { database: "sqlite", orm: "drizzle", api: "trpc", - frontend: ["tanstack-router", "native-nativewind"], + frontend: ["tanstack-router", "native-bare"], addons: ["turborepo"], examples: ["todo"], dbSetup: "none", diff --git a/apps/cli/test/deployment.test.ts b/apps/cli/test/deployment.test.ts index 8e435f4d5..b1489ef51 100644 --- a/apps/cli/test/deployment.test.ts +++ b/apps/cli/test/deployment.test.ts @@ -63,7 +63,7 @@ describe("Deployment Configurations", () => { projectName: "web-deploy-no-web-frontend-fail", webDeploy: "wrangler", serverDeploy: "none", - frontend: ["native-nativewind"], // Native frontend only + frontend: ["native-bare"], // Native frontend only backend: "hono", runtime: "bun", database: "sqlite", @@ -84,7 +84,7 @@ describe("Deployment Configurations", () => { projectName: "web-deploy-mixed-frontends", webDeploy: "wrangler", serverDeploy: "none", - frontend: ["tanstack-router", "native-nativewind"], + frontend: ["tanstack-router", "native-bare"], backend: "hono", runtime: "bun", database: "sqlite", @@ -541,7 +541,7 @@ describe("Deployment Configurations", () => { orm: "none", auth: "none", api: "none", - frontend: ["native-nativewind"], // Only native frontend + frontend: ["native-bare"], // Only native frontend addons: ["none"], examples: ["none"], dbSetup: "none", diff --git a/apps/cli/test/frontend.test.ts b/apps/cli/test/frontend.test.ts index 7879ba006..671b13981 100644 --- a/apps/cli/test/frontend.test.ts +++ b/apps/cli/test/frontend.test.ts @@ -14,7 +14,8 @@ describe("Frontend Configurations", () => { "tanstack-start", "next", "nuxt", - "native-nativewind", + "native-bare", + "native-uniwind", "native-unistyles", "svelte", "solid", @@ -24,7 +25,8 @@ describe("Frontend Configurations", () => { | "tanstack-start" | "next" | "nuxt" - | "native-nativewind" + | "native-bare" + | "native-uniwind" | "native-unistyles" | "svelte" | "solid" @@ -337,7 +339,7 @@ describe("Frontend Configurations", () => { it("should fail with multiple native frontends", async () => { const result = await runTRPCTest({ projectName: "multiple-native-fail", - frontend: ["native-nativewind", "native-unistyles"], + frontend: ["native-bare", "native-unistyles"], backend: "hono", runtime: "bun", database: "sqlite", @@ -358,7 +360,7 @@ describe("Frontend Configurations", () => { it("should work with one web + one native frontend", async () => { const result = await runTRPCTest({ projectName: "web-native-combo", - frontend: ["tanstack-router", "native-nativewind"], + frontend: ["tanstack-router", "native-bare"], backend: "hono", runtime: "bun", database: "sqlite", @@ -490,7 +492,7 @@ describe("Frontend Configurations", () => { it("should fail with web deploy but no web frontend", async () => { const result = await runTRPCTest({ projectName: "web-deploy-no-frontend-fail", - frontend: ["native-nativewind"], + frontend: ["native-bare"], webDeploy: "wrangler", backend: "hono", runtime: "bun", diff --git a/apps/cli/test/integration.test.ts b/apps/cli/test/integration.test.ts index 268c9be35..028655126 100644 --- a/apps/cli/test/integration.test.ts +++ b/apps/cli/test/integration.test.ts @@ -123,7 +123,7 @@ describe("Integration Tests - Real World Scenarios", () => { orm: "drizzle", auth: "better-auth", api: "trpc", - frontend: ["native-nativewind"], + frontend: ["native-bare"], addons: ["biome", "turborepo"], examples: ["todo"], dbSetup: "none", @@ -383,7 +383,7 @@ describe("Integration Tests - Real World Scenarios", () => { orm: "drizzle", auth: "none", api: "trpc", - frontend: ["native-nativewind"], + frontend: ["native-bare"], addons: ["pwa"], // PWA not compatible with native-only examples: ["none"], dbSetup: "none", @@ -450,7 +450,7 @@ describe("Integration Tests - Real World Scenarios", () => { orm: "drizzle", auth: "none", api: "trpc", - frontend: ["native-nativewind"], // Only native, no web + frontend: ["native-bare"], // Only native, no web addons: ["none"], examples: ["none"], dbSetup: "none", @@ -473,7 +473,7 @@ describe("Integration Tests - Real World Scenarios", () => { orm: "drizzle", auth: "better-auth", api: "trpc", - frontend: ["tanstack-router", "native-nativewind"], + frontend: ["tanstack-router", "native-bare"], addons: ["biome", "husky", "turborepo"], examples: ["todo", "ai"], dbSetup: "none", diff --git a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx index 4e1bf80f3..8c22bb61a 100644 --- a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx +++ b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx @@ -151,15 +151,17 @@ export function StackConfigurationCharts({ ? "hsl(var(--chart-4))" : entry.name === "nuxt" ? "hsl(var(--chart-5))" - : entry.name === "native-nativewind" + : entry.name === "native-bare" ? "hsl(var(--chart-6))" - : entry.name === "native-unistyles" + : entry.name === "native-uniwind" ? "hsl(var(--chart-7))" - : entry.name === "svelte" - ? "hsl(var(--chart-3))" - : entry.name === "solid" - ? "hsl(var(--chart-4))" - : "hsl(var(--chart-7))" + : entry.name === "native-unistyles" + ? "hsl(var(--chart-1))" + : entry.name === "svelte" + ? "hsl(var(--chart-3))" + : entry.name === "solid" + ? "hsl(var(--chart-4))" + : "hsl(var(--chart-7))" } /> ))} diff --git a/apps/web/src/app/(home)/analytics/_components/types.ts b/apps/web/src/app/(home)/analytics/_components/types.ts index 9428cc1b1..35efd910b 100644 --- a/apps/web/src/app/(home)/analytics/_components/types.ts +++ b/apps/web/src/app/(home)/analytics/_components/types.ts @@ -240,13 +240,17 @@ export const frontendConfig = { label: "Nuxt", color: "hsl(var(--chart-5))", }, - "native-nativewind": { - label: "Expo NativeWind", + "native-bare": { + label: "Expo Bare", color: "hsl(var(--chart-6))", }, + "native-uniwind": { + label: "Expo Uniwind", + color: "hsl(var(--chart-7))", + }, "native-unistyles": { label: "Expo Unistyles", - color: "hsl(var(--chart-7))", + color: "hsl(var(--chart-1))", }, svelte: { label: "Svelte", diff --git a/apps/web/src/app/(home)/new/_components/utils.ts b/apps/web/src/app/(home)/new/_components/utils.ts index 1c0086868..0cc76f7b7 100644 --- a/apps/web/src/app/(home)/new/_components/utils.ts +++ b/apps/web/src/app/(home)/new/_components/utils.ts @@ -105,7 +105,7 @@ export const analyzeStackCompatibility = ( ), ) || nextStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); const hasBetterAuthCompatibleFrontend = @@ -113,7 +113,7 @@ export const analyzeStackCompatibility = ( ["tanstack-router", "tanstack-start", "next"].includes(f), ) || nextStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (nextStack.auth === "clerk" && !hasClerkCompatibleFrontend) { @@ -882,7 +882,7 @@ export const analyzeStackCompatibility = ( ].includes(f), ) || nextStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (!hasClerkCompatibleFrontend) { @@ -911,12 +911,12 @@ export const analyzeStackCompatibility = ( ["tanstack-router", "tanstack-start", "next"].includes(f), ) || nextStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (!hasBetterAuthCompatibleFrontend) { notes.auth.notes.push( - "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (NativeWind/Unistyles). Auth will be set to 'None'.", + "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (Bare/UniWind/Unistyles). Auth will be set to 'None'.", ); notes.backend.notes.push( "Convex backend with Better-Auth requires compatible frontend. Auth will be disabled.", @@ -1259,11 +1259,11 @@ export const getDisabledReason = ( ["tanstack-router", "tanstack-start", "next"].includes(f), ) || currentStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (!hasBetterAuthCompatibleFrontend) { - return "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (NativeWind/Unistyles)."; + return "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (Bare/UniWind/Unistyles)."; } } } @@ -1351,11 +1351,11 @@ export const getDisabledReason = ( ), ) || finalStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (!hasClerkCompatibleFrontend) { - return "Clerk requires TanStack Router, React Router, TanStack Start, Next.js, or React Native frontend."; + return "Clerk requires TanStack Router, React Router, TanStack Start, Next.js, or React Native (Bare/UniWind/Unistyles) frontend."; } } @@ -1366,11 +1366,11 @@ export const getDisabledReason = ( ["tanstack-router", "tanstack-start", "next"].includes(f), ) || finalStack.nativeFrontend.some((f) => - ["native-nativewind", "native-unistyles"].includes(f), + ["native-bare", "native-uniwind", "native-unistyles"].includes(f), ); if (!hasBetterAuthCompatibleFrontend) { - return "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (NativeWind/Unistyles)."; + return "Better-Auth with Convex requires TanStack Router, TanStack Start, Next.js, or React Native (Bare/UniWind/Unistyles)."; } } } diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 678e34a8f..73197b95d 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -107,9 +107,18 @@ export const TECH_OPTIONS: Record< ], nativeFrontend: [ { - id: "native-nativewind", - name: "React Native + NativeWind", - description: "Expo with NativeWind (Tailwind)", + id: "native-bare", + name: "React Native + Bare", + description: "Expo with StyleSheet (no styling library)", + icon: `${ICON_BASE_URL}/expo.svg`, + color: "from-blue-400 to-blue-600", + className: "invert-0 dark:invert", + default: true, + }, + { + id: "native-uniwind", + name: "React Native + Uniwind", + description: "Expo with Uniwind (Tailwind CSS for React Native)", icon: `${ICON_BASE_URL}/expo.svg`, color: "from-purple-400 to-purple-600", className: "invert-0 dark:invert", @@ -118,7 +127,7 @@ export const TECH_OPTIONS: Record< { id: "native-unistyles", name: "React Native + Unistyles", - description: "Expo with Unistyles", + description: "Expo with Unistyles (type-safe styling)", icon: `${ICON_BASE_URL}/expo.svg`, color: "from-pink-400 to-pink-600", className: "invert-0 dark:invert", @@ -728,7 +737,7 @@ export const PRESET_TEMPLATES = [ stack: { projectName: "my-better-t-app", webFrontend: ["none"], - nativeFrontend: ["native-nativewind"], + nativeFrontend: ["native-bare"], runtime: "bun", backend: "hono", database: "sqlite", @@ -780,7 +789,7 @@ export const PRESET_TEMPLATES = [ stack: { projectName: "my-better-t-app", webFrontend: ["tanstack-router"], - nativeFrontend: ["native-nativewind"], + nativeFrontend: ["native-bare"], runtime: "bun", backend: "hono", database: "sqlite",