Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sour-brooms-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-expo-stack': minor
---

Added Convex + Better Auth in the authentication choices
8 changes: 8 additions & 0 deletions cli/src/commands/create-expo-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 15 additions & 6 deletions cli/src/templates/base/.gitignore.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 29 additions & 7 deletions cli/src/templates/base/App.tsx.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
<% if (props.authenticationPackage?.name === "convex") { %>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
</ConvexBetterAuthProvider>
<% } else {%>
<>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
</>
<%}%>
);
}

2 changes: 1 addition & 1 deletion cli/src/templates/base/app.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions cli/src/templates/base/babel.config.js.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion cli/src/templates/base/eslint.config.js.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
11 changes: 11 additions & 0 deletions cli/src/templates/base/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/templates/base/tsconfig.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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/*"]
<% } %>
Expand Down
4 changes: 4 additions & 0 deletions cli/src/templates/packages/convex/.env.local.ejs
Original file line number Diff line number Diff line change
@@ -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=
8 changes: 8 additions & 0 deletions cli/src/templates/packages/convex/convex/auth.config.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: "convex",
},
],
};
47 changes: 47 additions & 0 deletions cli/src/templates/packages/convex/convex/auth.ts.ejs
Original file line number Diff line number Diff line change
@@ -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<DataModel>(components.betterAuth);
const siteUrl = process.env.CONVEX_SITE_URL || "";

export const createAuth = (
ctx: GenericCtx<DataModel>,
{ 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);
},
});
7 changes: 7 additions & 0 deletions cli/src/templates/packages/convex/convex/convex.config.ts.ejs
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions cli/src/templates/packages/convex/convex/http.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";

const http = httpRouter();

authComponent.registerRoutes(http, createAuth);

export default http;
17 changes: 17 additions & 0 deletions cli/src/templates/packages/convex/lib/auth-client.ts.ejs
Original file line number Diff line number Diff line change
@@ -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(),
],
});
16 changes: 16 additions & 0 deletions cli/src/templates/packages/convex/metro.config.js.ejs
Original file line number Diff line number Diff line change
@@ -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;
<% } %>
63 changes: 46 additions & 17 deletions cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,41 +1,70 @@
<% if (props.stylingPackage?.name === "nativewind") { %>
import '../global.css';
<% } %>

<% if (props.stylingPackage?.name === "unistyles") { %>
import { useUnistyles } from "react-native-unistyles";
<% } %>
<% if (props.internalizationPackage?.name === "i18next") { %>
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") { %>
const { theme } = useUnistyles();
<% } %>

return (
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
<% if (props.authenticationPackage?.name === "convex") { %>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
</ConvexBetterAuthProvider>
<% } else {%>
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
<%}%>
);
}
Loading