diff --git a/.changeset/sour-brooms-fail.md b/.changeset/sour-brooms-fail.md new file mode 100644 index 00000000..86fc04c9 --- /dev/null +++ b/.changeset/sour-brooms-fail.md @@ -0,0 +1,5 @@ +--- +'create-expo-stack': minor +--- + +Added Convex + Better Auth in the authentication choices diff --git a/cli/src/commands/create-expo-stack.ts b/cli/src/commands/create-expo-stack.ts index 5e343827..0150f2a7 100644 --- a/cli/src/commands/create-expo-stack.ts +++ b/cli/src/commands/create-expo-stack.ts @@ -307,6 +307,14 @@ const command: GluegunCommand = { }); } + if (options.convex) { + // Add convex package + cliResults.packages.push({ + name: 'convex', + type: 'authentication' + }); + } + // State Management packages if (options.zustand) { // Add zustand package diff --git a/cli/src/templates/base/.gitignore.ejs b/cli/src/templates/base/.gitignore.ejs index 5040396e..f369dd07 100644 --- a/cli/src/templates/base/.gitignore.ejs +++ b/cli/src/templates/base/.gitignore.ejs @@ -9,12 +9,21 @@ npm-debug.* *.mobileprovision *.orig.* web-build/ -<% if (props.navigationPackage?.name === "expo-router") { %># expo router -expo-env.d.ts<% } %> -<% if (props.stylingPackage?.name === "tamagui") { %># tamagui -.tamagui/<% } %> -<% if ((props.authenticationPackage?.name === "supabase") || (props.authenticationPackage?.name === "firebase" || (props.analyticsPackage?.name === 'vexo-analytics'))) { %># firebase/supabase/vexo -.env<% } %> +<% if (props.navigationPackage?.name === "expo-router") { %> +# expo router +expo-env.d.ts +<% } %> +<% if (props.stylingPackage?.name === "tamagui") { %> +# tamagui +.tamagui/ +<% } %> +<% if ((props.authenticationPackage?.name === "supabase") || (props.authenticationPackage?.name === "firebase" || (props.analyticsPackage?.name === 'vexo-analytics'))) { %> +# firebase/supabase/vexo +.env +<% } %> +<% if (props.authenticationPackage?.name === "convex") { %> +.env.local +<% } %> ios android diff --git a/cli/src/templates/base/App.tsx.ejs b/cli/src/templates/base/App.tsx.ejs index e3dc807e..9fbc0dcc 100644 --- a/cli/src/templates/base/App.tsx.ejs +++ b/cli/src/templates/base/App.tsx.ejs @@ -21,17 +21,39 @@ import { StatusBar } from 'expo-status-bar'; vexo(process.env.EXPO_PUBLIC_VEXO_API_KEY); <% } %> +<% if (props.authenticationPackage?.name === "convex") { %> + import { ConvexReactClient } from "convex/react"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; + + const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, { + // Optionally pause queries until the user is authenticated + expectAuth: true, + unsavedChangesWarning: false, + }); +<% } %> export default function App() { return ( - <> - - <% if (props.internalizationPackage?.name === "i18next") { %> - - <% } %> - - + <% if (props.authenticationPackage?.name === "convex") { %> + + + <% if (props.internalizationPackage?.name === "i18next") { %> + + <% } %> + + + + <% } else {%> + <> + + <% if (props.internalizationPackage?.name === "i18next") { %> + + <% } %> + + + <%}%> ); } diff --git a/cli/src/templates/base/app.json.ejs b/cli/src/templates/base/app.json.ejs index 079ca7bb..f57e058f 100644 --- a/cli/src/templates/base/app.json.ejs +++ b/cli/src/templates/base/app.json.ejs @@ -6,8 +6,8 @@ <% if (props.stylingPackage?.name === "unistyles") { %> "newArchEnabled": true, <% } %> + "scheme": "<%= props.projectName %>", <% if (props.navigationPackage?.name === 'expo-router') { %> - "scheme": "<%= props.projectName %>", "platforms": ["ios", "android"], "web": { "bundler": "metro", diff --git a/cli/src/templates/base/babel.config.js.ejs b/cli/src/templates/base/babel.config.js.ejs index cc589bff..bb0655cb 100644 --- a/cli/src/templates/base/babel.config.js.ejs +++ b/cli/src/templates/base/babel.config.js.ejs @@ -12,6 +12,19 @@ module.exports = function(api) { ]); <% } %> + <% if (props.authenticationPackage?.name === "convex") { %> + plugins.push([ + "module-resolver", + { + alias: { + "better-auth/react": "./node_modules/better-auth/dist/client/react/index.cjs", + "better-auth/client/plugins": "./node_modules/better-auth/dist/client/plugins/index.cjs", + "@better-auth/expo/client": "./node_modules/@better-auth/expo/dist/client.cjs", + }, + }, + ]); + <% } %> + plugins.push('react-native-worklets/plugin'); return { diff --git a/cli/src/templates/base/eslint.config.js.ejs b/cli/src/templates/base/eslint.config.js.ejs index 514d7086..5e48bb76 100644 --- a/cli/src/templates/base/eslint.config.js.ejs +++ b/cli/src/templates/base/eslint.config.js.ejs @@ -5,7 +5,12 @@ const expoConfig = require('eslint-config-expo/flat'); module.exports = defineConfig([ expoConfig, { - ignores: ['dist/*'], + ignores: [ + 'dist/*', + <% if (props.authenticationPackage?.name === "convex") { %> + 'convex/*' + <% } %> + ], }, { rules: { diff --git a/cli/src/templates/base/package.json.ejs b/cli/src/templates/base/package.json.ejs index ddeafbf0..79f3389f 100644 --- a/cli/src/templates/base/package.json.ejs +++ b/cli/src/templates/base/package.json.ejs @@ -116,6 +116,14 @@ "firebase": "^10.5.2", <% } %> + <% if (props.authenticationPackage?.name === "convex") { %> + "@convex-dev/better-auth": "^0.9.7", + "@better-auth/expo": "1.3.34", + "better-auth": "1.3.34", + "convex": "^1.29.3", + "expo-secure-store": "~15.0.7", + <% } %> + <% if (props.internalizationPackage?.name === "i18next") { %> "i18next": "^23.7.20", "react-i18next": "^14.0.1", @@ -138,6 +146,9 @@ "devDependencies": { "@babel/core": "^7.20.0", "@types/react": "~19.1.10", + <% if (props.authenticationPackage?.name === "convex") { %> + "babel-plugin-module-resolver": "^5.0.2", + <% } %> "eslint": "^9.25.1", "eslint-config-expo": "~10.0.0", "eslint-config-prettier": "^10.1.2", diff --git a/cli/src/templates/base/tsconfig.json.ejs b/cli/src/templates/base/tsconfig.json.ejs index 92548be3..10a39f18 100644 --- a/cli/src/templates/base/tsconfig.json.ejs +++ b/cli/src/templates/base/tsconfig.json.ejs @@ -11,7 +11,7 @@ <% if (props.navigationPackage?.name === "expo-router" && props.flags.importAlias === true) { %> "@/*": ["*"] <% } else if (props.flags.importAlias === true) { %> - "@/*": ["src/*"] + "@/*": ["src/*", "*"] <% } else { %> "<%= props.flags.importAlias %>": ["src/*"] <% } %> diff --git a/cli/src/templates/packages/convex/.env.local.ejs b/cli/src/templates/packages/convex/.env.local.ejs new file mode 100644 index 00000000..2c38b745 --- /dev/null +++ b/cli/src/templates/packages/convex/.env.local.ejs @@ -0,0 +1,4 @@ +# Start by running: npx convex dev, to login and create your Convex backend +# Then set EXPO_PUBLIC_CONVEX_URL_SITE as the same as EXPO_PUBLIC_CONVEX_URL but ends in .site, it can also be found as the HTTP Actions URL in the URL & Deploy Key tab of your Convex dashboard +# Finaly run this command once to set your better auth secret in your convex dashboard: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32) +EXPO_PUBLIC_CONVEX_SITE_URL= \ No newline at end of file diff --git a/cli/src/templates/packages/convex/convex/auth.config.ts.ejs b/cli/src/templates/packages/convex/convex/auth.config.ts.ejs new file mode 100644 index 00000000..18740698 --- /dev/null +++ b/cli/src/templates/packages/convex/convex/auth.config.ts.ejs @@ -0,0 +1,8 @@ +export default { + providers: [ + { + domain: process.env.CONVEX_SITE_URL, + applicationID: "convex", + }, + ], +}; \ No newline at end of file diff --git a/cli/src/templates/packages/convex/convex/auth.ts.ejs b/cli/src/templates/packages/convex/convex/auth.ts.ejs new file mode 100644 index 00000000..56758dba --- /dev/null +++ b/cli/src/templates/packages/convex/convex/auth.ts.ejs @@ -0,0 +1,47 @@ +import { createClient, type GenericCtx } from "@convex-dev/better-auth"; +import { convex } from "@convex-dev/better-auth/plugins"; +import { betterAuth } from "better-auth"; +import { expo } from '@better-auth/expo'; +import { components } from "./_generated/api"; +import { DataModel } from "./_generated/dataModel"; +import { query } from "./_generated/server"; + +// The component client has methods needed for integrating Convex with Better Auth, +// as well as helper methods for general use. +export const authComponent = createClient(components.betterAuth); +const siteUrl = process.env.CONVEX_SITE_URL || ""; + +export const createAuth = ( + ctx: GenericCtx, + { optionsOnly } = { optionsOnly: false }, +) => { + return betterAuth({ + baseURL: siteUrl, + // disable logging when createAuth is called just to generate options. + // this is not required, but there's a lot of noise in logs without it. + logger: { + disabled: optionsOnly, + }, + trustedOrigins: ["your-scheme://", siteUrl], + database: authComponent.adapter(ctx), + // Configure simple, non-verified email/password to get started + emailAndPassword: { + enabled: true, + requireEmailVerification: false, + }, + plugins: [ + // The Expo and Convex plugins are required + expo(), + convex(), + ], + }); +}; + +// Example function for getting the current user +// Feel free to edit, omit, etc. +export const getCurrentUser = query({ + args: {}, + handler: async (ctx) => { + return authComponent.getAuthUser(ctx); + }, +}); \ No newline at end of file diff --git a/cli/src/templates/packages/convex/convex/convex.config.ts.ejs b/cli/src/templates/packages/convex/convex/convex.config.ts.ejs new file mode 100644 index 00000000..a49fc735 --- /dev/null +++ b/cli/src/templates/packages/convex/convex/convex.config.ts.ejs @@ -0,0 +1,7 @@ +import { defineApp } from "convex/server"; +import betterAuth from "@convex-dev/better-auth/convex.config"; + +const app = defineApp(); +app.use(betterAuth); + +export default app; \ No newline at end of file diff --git a/cli/src/templates/packages/convex/convex/http.ts.ejs b/cli/src/templates/packages/convex/convex/http.ts.ejs new file mode 100644 index 00000000..e84c06a0 --- /dev/null +++ b/cli/src/templates/packages/convex/convex/http.ts.ejs @@ -0,0 +1,8 @@ +import { httpRouter } from "convex/server"; +import { authComponent, createAuth } from "./auth"; + +const http = httpRouter(); + +authComponent.registerRoutes(http, createAuth); + +export default http; \ No newline at end of file diff --git a/cli/src/templates/packages/convex/lib/auth-client.ts.ejs b/cli/src/templates/packages/convex/lib/auth-client.ts.ejs new file mode 100644 index 00000000..ab3d25df --- /dev/null +++ b/cli/src/templates/packages/convex/lib/auth-client.ts.ejs @@ -0,0 +1,17 @@ +import { createAuthClient } from "better-auth/react"; +import { convexClient } from "@convex-dev/better-auth/client/plugins"; +import { expoClient } from '@better-auth/expo/client'; +import Constants from 'expo-constants'; +import * as SecureStore from 'expo-secure-store'; + +export const authClient = createAuthClient({ + baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL, + plugins: [ + expoClient({ + scheme: Constants.expoConfig?.scheme as string, + storagePrefix: Constants.expoConfig?.scheme as string, + storage: SecureStore, + }), + convexClient(), + ], +}); \ No newline at end of file diff --git a/cli/src/templates/packages/convex/metro.config.js.ejs b/cli/src/templates/packages/convex/metro.config.js.ejs new file mode 100644 index 00000000..351416f7 --- /dev/null +++ b/cli/src/templates/packages/convex/metro.config.js.ejs @@ -0,0 +1,16 @@ +const { getDefaultConfig } = require('expo/metro-config'); +<% if (props.stylingPackage?.name === "nativewind") { %> + const { withNativeWind } = require("nativewind/metro"); +<% } %> + +/** @type {import('expo/metro-config').MetroConfig} */ +// eslint-disable-next-line no-undef +const config = getDefaultConfig(__dirname); + +config.resolver.unstable_enablePackageExports = true; + +<% if (props.stylingPackage?.name === "nativewind") { %> + module.exports = withNativeWind(config, { input: "./global.css" }); +<% } else { %> + module.exports = config; +<% } %> diff --git a/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs b/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs index eac81954..52bdea6c 100644 --- a/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs +++ b/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs @@ -1,7 +1,6 @@ <% if (props.stylingPackage?.name === "nativewind") { %> import '../global.css'; <% } %> - <% if (props.stylingPackage?.name === "unistyles") { %> import { useUnistyles } from "react-native-unistyles"; <% } %> @@ -9,12 +8,22 @@ import '../translation'; <% } %> import { Stack } from "expo-router"; - <% if (props.analyticsPackage?.name === "vexo-analytics") { %> import { vexo } from 'vexo-analytics'; vexo(process.env.EXPO_PUBLIC_VEXO_API_KEY); <% } %> +<% if (props.authenticationPackage?.name === "convex") { %> + import { ConvexReactClient } from "convex/react"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; + + const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, { + // Optionally pause queries until the user is authenticated + expectAuth: true, + unsavedChangesWarning: false, + }); +<% } %> export default function Layout() { <% if (props.stylingPackage?.name === "unistyles") { %> @@ -22,20 +31,40 @@ export default function Layout() { <% } %> return ( - <% if (props.stylingPackage?.name === "unistyles") { %> - - <% } else { %> - - <% } %> + <% if (props.authenticationPackage?.name === "convex") { %> + + <% if (props.stylingPackage?.name === "unistyles") { %> + + <% } else { %> + + <% } %> + + <% } else {%> + <% if (props.stylingPackage?.name === "unistyles") { %> + + <% } else { %> + + <% } %> + <%}%> ); } diff --git a/cli/src/templates/packages/react-navigation/App.tsx.ejs b/cli/src/templates/packages/react-navigation/App.tsx.ejs index 5fe4be17..aafd2c1c 100644 --- a/cli/src/templates/packages/react-navigation/App.tsx.ejs +++ b/cli/src/templates/packages/react-navigation/App.tsx.ejs @@ -28,33 +28,59 @@ import "react-native-gesture-handler"; <% } else { %> import Navigation from "./navigation"; <% } %> +<% if (props.authenticationPackage?.name === "convex") { %> + import { ConvexReactClient } from "convex/react"; + import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; + import { authClient } from "@/lib/auth-client"; + + const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, { + // Optionally pause queries until the user is authenticated + expectAuth: true, + unsavedChangesWarning: false, + }); +<% } %> export default function App() { - <% if (props.stylingPackage?.name === "unistyles") { %> - const { theme, rt } = useUnistyles(); + <% if (props.stylingPackage?.name === "unistyles") { %> + const { theme: uniTheme, rt } = useUnistyles(); const baseTheme = rt.colorScheme === 'dark' ? DarkTheme : DefaultTheme; - const mergedTheme = useMemo(() => ({ + const theme = useMemo(() => ({ ...baseTheme, colors: { ...baseTheme.colors, - background: theme.colors.background, - text: theme.colors.typography, - primary: theme.colors.astral, - secondary: theme.colors.cornflowerBlue, - border: theme.colors.limedSpruce, - card: theme.colors.background, - notification: theme.colors.astral, + background: uniTheme.colors.background, + text: uniTheme.colors.typography, + primary: uniTheme.colors.astral, + secondary: uniTheme.colors.cornflowerBlue, + border: uniTheme.colors.limedSpruce, + card: uniTheme.colors.background, + notification: uniTheme.colors.astral, }, - }), [baseTheme, theme.colors.background, theme.colors.typography, theme.colors.astral, theme.colors.cornflowerBlue, theme.colors.limedSpruce]); - - return ; - <% } else if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet") { %> - const colorScheme = useColorScheme(); + }), [baseTheme, uniTheme.colors.background, uniTheme.colors.typography, uniTheme.colors.astral, uniTheme.colors.cornflowerBlue, uniTheme.colors.limedSpruce]); + <% } %> + <% if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet") { %> + const colorScheme = useColorScheme(); const theme = useMemo(() => colorScheme === 'dark' ? DarkTheme : DefaultTheme, [colorScheme]); + <% } %> - return ; - <% } else { %> - return ; - <% } %> + return ( + <% if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet" || props.stylingPackage?.name === "unistyles") { %> + <% if (props.authenticationPackage?.name === "convex") { %> + + + + <% } else {%> + + <%}%> + <% } else { %> + <% if (props.authenticationPackage?.name === "convex") { %> + + + + <% } else {%> + + <%}%> + <% } %> + ) } diff --git a/cli/src/types.ts b/cli/src/types.ts index db4b548e..9c50efd9 100644 --- a/cli/src/types.ts +++ b/cli/src/types.ts @@ -24,13 +24,14 @@ export const availablePackages = [ 'reactnavigation', 'stylesheet', 'supabase', + 'convex', 'unistyles', 'i18next', 'zustand', 'vexo-analytics' ] as const; -export type AuthenticationSelect = 'supabase' | 'firebase' | undefined; +export type AuthenticationSelect = 'supabase' | 'firebase' | 'convex' | undefined; export type NavigationSelect = 'react-navigation' | 'expo-router' | undefined; diff --git a/cli/src/utilities/configureProjectFiles.ts b/cli/src/utilities/configureProjectFiles.ts index c1d016e9..4f0ab2e1 100644 --- a/cli/src/utilities/configureProjectFiles.ts +++ b/cli/src/utilities/configureProjectFiles.ts @@ -288,7 +288,7 @@ export function configureProjectFiles( files = [...files, ...supabaseFiles]; } - // add supabase files if needed + // add firebase files if needed if (authenticationPackage?.name === 'firebase') { const firebaseFiles = [ 'packages/firebase/utils/firebase.ts.ejs', @@ -299,6 +299,21 @@ export function configureProjectFiles( files = [...files, ...firebaseFiles]; } + // add convex files if needed + if (authenticationPackage?.name === 'convex') { + const convexFiles = [ + 'packages/convex/convex/auth.config.ts.ejs', + 'packages/convex/convex/auth.ts.ejs', + 'packages/convex/convex/http.ts.ejs', + 'packages/convex/convex/convex.config.ts.ejs', + 'packages/convex/lib/auth-client.ts.ejs', + 'packages/convex/.env.local.ejs', + 'packages/convex/metro.config.js.ejs' + ]; + + files = [...files, ...convexFiles]; + } + // add vexo analytics files if needed if (analyticsPackage?.name == 'vexo-analytics') { const vexoFiles = ['packages/vexo-analytics/.env.ejs']; diff --git a/cli/src/utilities/generateProjectFiles.ts b/cli/src/utilities/generateProjectFiles.ts index e58439f3..a8a008e4 100644 --- a/cli/src/utilities/generateProjectFiles.ts +++ b/cli/src/utilities/generateProjectFiles.ts @@ -28,6 +28,10 @@ export function generateProjectFiles( target = target.replace('packages/firebase/', ''); } + if (authenticationPackage?.name === 'convex') { + target = target.replace('packages/convex/', ''); + } + //state management if (stateManagementPackage?.name === 'zustand') { target = target.replace('packages/zustand/', ''); diff --git a/cli/src/utilities/runCLI.ts b/cli/src/utilities/runCLI.ts index bc338e9f..fcb4120d 100644 --- a/cli/src/utilities/runCLI.ts +++ b/cli/src/utilities/runCLI.ts @@ -353,7 +353,8 @@ export async function runCLI(toolbox: Toolbox, projectName: string): Promise = [ { value: undefined, label: 'None' }, { value: 'supabase', label: 'Supabase' }, - { value: 'firebase', label: 'Firebase' } + { value: 'firebase', label: 'Firebase' }, + { value: 'convex', label: 'Convex + Better Auth' } ]; const authenticationSelect = await select({