Update expo app to make it somewhat functional
This commit is contained in:
83
AGENTS.md
83
AGENTS.md
@@ -1247,29 +1247,45 @@ See the 4-step checklist in [Section 5](#5-environment-variables--complete-refer
|
||||
|
||||
---
|
||||
|
||||
## 14. Expo App — Known Issues, Do Not Touch
|
||||
## 14. Expo App — Scaffold Only, Not Production-Ready
|
||||
|
||||
The `apps/expo/` directory exists as a placeholder for future mobile development.
|
||||
It is currently non-functional — it still contains the original T3 Turbo template
|
||||
code and has not been migrated to use Convex or `@convex-dev/auth`.
|
||||
The broken T3 Turbo template code has been cleaned up — all `@acme/*` references,
|
||||
tRPC, and `better-auth` have been removed and replaced with the correct Convex patterns.
|
||||
The app will now load, but it is still a bare scaffold with no real features.
|
||||
|
||||
**Do not work on the Expo app unless explicitly asked.** If you are asked to work on it,
|
||||
be aware that the following are all broken and will need to be addressed:
|
||||
### Current state
|
||||
|
||||
| File | Issue |
|
||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `src/utils/api.tsx` | Imports `AppRouter` from `@acme/api` — package does not exist. Uses tRPC, which is not used anywhere in this repo. |
|
||||
| `src/utils/auth.ts` | Uses `better-auth` / `@better-auth/expo` instead of `@convex-dev/auth` |
|
||||
| `src/app/index.tsx` | Old T3 Turbo template code using tRPC mutations and better-auth session patterns |
|
||||
| `src/app/post/[id].tsx` | Same — tRPC and better-auth patterns throughout |
|
||||
| `postcss.config.js` | References `@acme/tailwind-config` — package does not exist |
|
||||
| `src/styles.css` | Imports from `@acme/tailwind-config` — package does not exist |
|
||||
| `eslint.config.mts` | Imports from `@acme/eslint-config` — package does not exist |
|
||||
| `eas.json` | Specifies `pnpm 9.15.4` as the package manager — should be bun |
|
||||
The app is wired up with the correct providers and patterns:
|
||||
|
||||
When the time comes to properly implement the Expo app, the right approach is to start
|
||||
fresh with Convex + `@convex-dev/auth` patterns (mirroring how the Next.js app works),
|
||||
not to try to incrementally fix the broken T3 Turbo code.
|
||||
- **`src/app/_layout.tsx`** — `ConvexAuthProvider` wraps the root, initialized with
|
||||
a `ConvexReactClient` pointed at the self-hosted backend
|
||||
- **`src/utils/convex.ts`** — Convex client setup, derives the backend URL from the
|
||||
Expo dev server host in development; reads from `app.config.ts` `extra.convexUrl`
|
||||
in production
|
||||
- **`src/app/index.tsx`** — A minimal home screen that reads auth state via
|
||||
`useConvexAuth()` and queries the current user via `api.auth.getUser`
|
||||
- **`src/app/post/[id].tsx`** — A placeholder detail screen (route structure kept intact)
|
||||
- **`src/utils/session-store.ts`** — `expo-secure-store` helpers for token storage
|
||||
- **`src/utils/base-url.ts`** — Dev server localhost detection
|
||||
|
||||
### What still needs to be built
|
||||
|
||||
- Sign-in / sign-up screens (mirror the Next.js `(auth)/sign-in` flow, adapted for RN)
|
||||
- Authenticated navigation / route protection (check `isAuthenticated` in layout)
|
||||
- Any actual app screens and Convex queries for your specific project
|
||||
|
||||
### Production URL configuration
|
||||
|
||||
For production builds, set `convexUrl` in `app.config.ts` `extra`:
|
||||
|
||||
```typescript
|
||||
extra: {
|
||||
convexUrl: 'https://api.convex.example.com',
|
||||
},
|
||||
```
|
||||
|
||||
The client in `src/utils/convex.ts` will prefer this over the dev-server-derived URL.
|
||||
|
||||
---
|
||||
|
||||
@@ -1290,16 +1306,7 @@ native fix for this or if a custom update script is needed.
|
||||
|
||||
---
|
||||
|
||||
**2. Password validation mismatch** ✅ Fixed
|
||||
|
||||
`validatePassword()` in `packages/backend/convex/custom/auth/providers/password.ts`
|
||||
now fully matches `PASSWORD_REGEX` in `packages/backend/types/auth.ts`. Both enforce:
|
||||
minimum 8 characters, maximum 100, no whitespace, at least one digit, one lowercase
|
||||
letter, one uppercase letter, and at least one special character (`[\p{P}\p{S}]`).
|
||||
|
||||
---
|
||||
|
||||
**3. Root `.dockerignore` includes `.env` in build context** ℹ️ Known, works intentionally
|
||||
**2. Root `.dockerignore` includes `.env` in build context** ℹ️ Known, works intentionally
|
||||
|
||||
The root `.dockerignore` has `.env` commented out, meaning the `.env` file is sent to
|
||||
the Docker build context. This is how build-time env vars (Sentry token, `NEXT_PUBLIC_*`)
|
||||
@@ -1307,12 +1314,9 @@ reach the Next.js build step. It's a known imperfect approach but fully function
|
||||
A proper solution would use Docker build args (`ARG`) or multi-stage secrets, but this
|
||||
requires careful restructuring to avoid breaking Sentry source map uploads.
|
||||
|
||||
The redundant `docker/.dockerignore` has been deleted — it was never read by Docker
|
||||
since the build context is always the repo root.
|
||||
|
||||
---
|
||||
|
||||
**4. In-memory IP banning resets on restart** ℹ️ Future enhancement
|
||||
**3. In-memory IP banning resets on restart** ℹ️ Future enhancement
|
||||
|
||||
`src/lib/proxy/ban-sus-ips.ts` stores bans in a JavaScript `Map`. Bans reset whenever
|
||||
the Next.js server restarts (container restart, redeploy, etc.). A more robust solution
|
||||
@@ -1321,7 +1325,7 @@ job) or to Redis.
|
||||
|
||||
---
|
||||
|
||||
**5. `apps/next/src/lib/metadata.ts` has hardcoded branding** ⚠️ Template concern
|
||||
**4. `apps/next/src/lib/metadata.ts` has hardcoded branding** ⚠️ Template concern
|
||||
|
||||
When using this as a template for a new project, `metadata.ts` must be updated:
|
||||
the title template (`'%s | Convex Monorepo'`) and any other project-specific strings.
|
||||
@@ -1329,7 +1333,7 @@ Same for `next.config.js` `images.remotePatterns` (currently `*.gbrown.org`).
|
||||
|
||||
---
|
||||
|
||||
**6. No CI/CD** ℹ️ Future enhancement
|
||||
**5. No CI/CD** ℹ️ Future enhancement
|
||||
|
||||
There is no `.github/workflows/` directory. All deployment is done manually via SSH.
|
||||
A future enhancement would add GitHub Actions (or Gitea Actions, since this repo is
|
||||
@@ -1337,7 +1341,7 @@ on Gitea) for automated lint and typecheck on pull requests.
|
||||
|
||||
---
|
||||
|
||||
**7. Scripts organization** ℹ️ Future exploration
|
||||
**6. Scripts organization** ℹ️ Future exploration
|
||||
|
||||
Currently:
|
||||
|
||||
@@ -1351,14 +1355,7 @@ moving everything to `docker/scripts/`.
|
||||
|
||||
---
|
||||
|
||||
**8. Expo `eas.json` specifies pnpm** ⚠️ Low priority (Expo is WIP anyway)
|
||||
|
||||
`apps/expo/eas.json` specifies `pnpm 9.15.4` as the package manager. Should specify
|
||||
bun. This is low priority since the Expo app is not in active development.
|
||||
|
||||
---
|
||||
|
||||
**9. React Email templates not yet implemented** ℹ️ Future enhancement
|
||||
**7. React Email templates not yet implemented** ℹ️ Future enhancement
|
||||
|
||||
`@react-email/components` and `react-email` are in `packages/backend/package.json` as
|
||||
planned dependencies. The current `usesend.ts` uses inline HTML strings for email
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"build": {
|
||||
"base": {
|
||||
"node": "22.12.0",
|
||||
"pnpm": "9.15.4",
|
||||
"bun": "1.3.10",
|
||||
"ios": {
|
||||
"resourceClass": "m-medium"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { baseConfig } from '@acme/eslint-config/base';
|
||||
import { reactConfig } from '@acme/eslint-config/react';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
|
||||
import { baseConfig } from '@gib/eslint-config/base';
|
||||
import { reactConfig } from '@gib/eslint-config/react';
|
||||
|
||||
export default defineConfig(
|
||||
{
|
||||
ignores: ['.expo/**', 'expo-plugins/**'],
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
"react-native-safe-area-context": "~5.6.2",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "~0.21.2",
|
||||
"react-native-worklets": "~0.5.2",
|
||||
"superjson": "2.2.3"
|
||||
"react-native-worklets": "~0.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gib/eslint-config": "workspace:*",
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = require('@acme/tailwind-config/postcss-config');
|
||||
module.exports = require('@gib/tailwind-config/postcss-config');
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { Stack } from 'expo-router';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ConvexAuthProvider } from '@convex-dev/auth/react';
|
||||
|
||||
import { queryClient } from '~/utils/api';
|
||||
import { convex } from '~/utils/convex';
|
||||
|
||||
import '../styles.css';
|
||||
|
||||
// This is the main layout of the app
|
||||
// It wraps your pages with the providers they need
|
||||
export default function RootLayout() {
|
||||
const RootLayout = () => {
|
||||
const colorScheme = useColorScheme();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/*
|
||||
The Stack component displays the current page.
|
||||
It also allows you to configure your screens
|
||||
*/}
|
||||
<ConvexAuthProvider client={convex}>
|
||||
<Stack
|
||||
screenOptions={{
|
||||
headerStyle: {
|
||||
backgroundColor: '#c03484',
|
||||
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||
},
|
||||
headerTintColor: colorScheme === 'dark' ? '#fafaf9' : '#1c1917',
|
||||
contentStyle: {
|
||||
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
|
||||
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<StatusBar />
|
||||
</QueryClientProvider>
|
||||
<StatusBar style='auto' />
|
||||
</ConvexAuthProvider>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
|
||||
@@ -1,172 +1,54 @@
|
||||
import { useState } from 'react';
|
||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Link, Stack } from 'expo-router';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Stack } from 'expo-router';
|
||||
import { useAuthActions } from '@convex-dev/auth/react';
|
||||
import { useConvexAuth, useQuery } from 'convex/react';
|
||||
|
||||
import type { RouterOutputs } from '~/utils/api';
|
||||
import { trpc } from '~/utils/api';
|
||||
import { authClient } from '~/utils/auth';
|
||||
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||
|
||||
function PostCard(props: {
|
||||
post: RouterOutputs['post']['all'][number];
|
||||
onDelete: () => void;
|
||||
}) {
|
||||
return (
|
||||
<View className='bg-muted flex flex-row rounded-lg p-4'>
|
||||
<View className='grow'>
|
||||
<Link
|
||||
asChild
|
||||
href={{
|
||||
pathname: '/post/[id]',
|
||||
params: { id: props.post.id },
|
||||
}}
|
||||
>
|
||||
<Pressable className=''>
|
||||
<Text className='text-primary text-xl font-semibold'>
|
||||
{props.post.title}
|
||||
</Text>
|
||||
<Text className='text-foreground mt-2'>{props.post.content}</Text>
|
||||
</Pressable>
|
||||
</Link>
|
||||
</View>
|
||||
<Pressable onPress={props.onDelete}>
|
||||
<Text className='text-primary font-bold uppercase'>Delete</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function CreatePost() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
const [content, setContent] = useState('');
|
||||
|
||||
const { mutate, error } = useMutation(
|
||||
trpc.post.create.mutationOptions({
|
||||
async onSuccess() {
|
||||
setTitle('');
|
||||
setContent('');
|
||||
await queryClient.invalidateQueries(trpc.post.all.queryFilter());
|
||||
},
|
||||
}),
|
||||
);
|
||||
const Index = () => {
|
||||
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||
const { signOut } = useAuthActions();
|
||||
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
|
||||
|
||||
return (
|
||||
<View className='mt-4 flex gap-2'>
|
||||
<TextInput
|
||||
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
placeholder='Title'
|
||||
/>
|
||||
{error?.data?.zodError?.fieldErrors.title && (
|
||||
<Text className='text-destructive mb-2'>
|
||||
{error.data.zodError.fieldErrors.title}
|
||||
<SafeAreaView className='bg-background flex-1'>
|
||||
<Stack.Screen options={{ title: 'Home' }} />
|
||||
<View className='flex-1 items-center justify-center gap-4 p-6'>
|
||||
<Text className='text-foreground text-4xl font-bold'>
|
||||
Convex Monorepo
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
|
||||
value={content}
|
||||
onChangeText={setContent}
|
||||
placeholder='Content'
|
||||
/>
|
||||
{error?.data?.zodError?.fieldErrors.content && (
|
||||
<Text className='text-destructive mb-2'>
|
||||
{error.data.zodError.fieldErrors.content}
|
||||
<Text className='text-muted-foreground text-center text-base'>
|
||||
Your self-hosted Expo + Convex starter
|
||||
</Text>
|
||||
)}
|
||||
<Pressable
|
||||
className='bg-primary flex items-center rounded-sm p-2'
|
||||
onPress={() => {
|
||||
mutate({
|
||||
title,
|
||||
content,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Text className='text-foreground'>Create</Text>
|
||||
</Pressable>
|
||||
{error?.data?.code === 'UNAUTHORIZED' && (
|
||||
<Text className='text-destructive mt-2'>
|
||||
You need to be logged in to create a post
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileAuth() {
|
||||
const { data: session } = authClient.useSession();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text className='text-foreground pb-2 text-center text-xl font-semibold'>
|
||||
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
|
||||
{isLoading ? (
|
||||
<Text className='text-muted-foreground'>Loading...</Text>
|
||||
) : isAuthenticated ? (
|
||||
<View className='w-full gap-3'>
|
||||
<Text className='text-foreground text-center text-lg'>
|
||||
Welcome{user?.name ? `, ${user.name}` : ''}!
|
||||
</Text>
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
session
|
||||
? authClient.signOut()
|
||||
: authClient.signIn.social({
|
||||
provider: 'discord',
|
||||
callbackURL: '/',
|
||||
})
|
||||
}
|
||||
className='bg-primary flex items-center rounded-sm p-2'
|
||||
className='bg-primary items-center rounded-md p-3'
|
||||
onPress={() => void signOut()}
|
||||
>
|
||||
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
|
||||
<Text className='text-primary-foreground font-semibold'>
|
||||
Sign Out
|
||||
</Text>
|
||||
</Pressable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const postQuery = useQuery(trpc.post.all.queryOptions());
|
||||
|
||||
const deletePostMutation = useMutation(
|
||||
trpc.post.delete.mutationOptions({
|
||||
onSettled: () =>
|
||||
queryClient.invalidateQueries(trpc.post.all.queryFilter()),
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView className='bg-background'>
|
||||
{/* Changes page title visible on the header */}
|
||||
<Stack.Screen options={{ title: 'Home Page' }} />
|
||||
<View className='bg-background h-full w-full p-4'>
|
||||
<Text className='text-foreground pb-2 text-center text-5xl font-bold'>
|
||||
Create <Text className='text-primary'>T3</Text> Turbo
|
||||
</Text>
|
||||
|
||||
<MobileAuth />
|
||||
|
||||
<View className='py-2'>
|
||||
<Text className='text-primary font-semibold italic'>
|
||||
Press on a post
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<LegendList
|
||||
data={postQuery.data ?? []}
|
||||
estimatedItemSize={20}
|
||||
keyExtractor={(item) => item.id}
|
||||
ItemSeparatorComponent={() => <View className='h-2' />}
|
||||
renderItem={(p) => (
|
||||
<PostCard
|
||||
post={p.item}
|
||||
onDelete={() => deletePostMutation.mutate(p.item.id)}
|
||||
/>
|
||||
) : (
|
||||
<View className='w-full gap-3'>
|
||||
<Text className='text-muted-foreground text-center'>
|
||||
Sign in to get started
|
||||
</Text>
|
||||
{/* Add sign-in UI here — see apps/next/src/app/(auth)/sign-in for patterns */}
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
<CreatePost />
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Index;
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import { SafeAreaView, Text, View } from 'react-native';
|
||||
import { Stack, useGlobalSearchParams } from 'expo-router';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Text, View } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { Stack, useLocalSearchParams } from 'expo-router';
|
||||
|
||||
import { trpc } from '~/utils/api';
|
||||
|
||||
export default function Post() {
|
||||
const { id } = useGlobalSearchParams<{ id: string }>();
|
||||
const { data } = useQuery(trpc.post.byId.queryOptions({ id }));
|
||||
|
||||
if (!data) return null;
|
||||
const Post = () => {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
|
||||
return (
|
||||
<SafeAreaView className='bg-background'>
|
||||
<Stack.Screen options={{ title: data.title }} />
|
||||
<View className='h-full w-full p-4'>
|
||||
<Text className='text-primary py-2 text-3xl font-bold'>
|
||||
{data.title}
|
||||
<SafeAreaView className='bg-background flex-1'>
|
||||
<Stack.Screen options={{ title: 'Post' }} />
|
||||
<View className='flex-1 p-4'>
|
||||
<Text className='text-foreground text-2xl font-bold'>Post {id}</Text>
|
||||
<Text className='text-muted-foreground mt-2'>
|
||||
Implement your post detail screen here using Convex queries.
|
||||
</Text>
|
||||
<Text className='text-foreground py-4'>{data.content}</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default Post;
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'nativewind/theme';
|
||||
@import '@acme/tailwind-config/theme';
|
||||
@import '@gib/tailwind-config/theme';
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import type { AppRouter } from '@acme/api';
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
import { createTRPCClient, httpBatchLink, loggerLink } from '@trpc/client';
|
||||
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
|
||||
import superjson from 'superjson';
|
||||
|
||||
import { authClient } from './auth';
|
||||
import { getBaseUrl } from './base-url';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// ...
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* A set of typesafe hooks for consuming your API.
|
||||
*/
|
||||
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
||||
client: createTRPCClient({
|
||||
links: [
|
||||
loggerLink({
|
||||
enabled: (opts) =>
|
||||
process.env.NODE_ENV === 'development' ||
|
||||
(opts.direction === 'down' && opts.result instanceof Error),
|
||||
colorMode: 'ansi',
|
||||
}),
|
||||
httpBatchLink({
|
||||
transformer: superjson,
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
headers() {
|
||||
const headers = new Map<string, string>();
|
||||
headers.set('x-trpc-source', 'expo-react');
|
||||
|
||||
const cookies = authClient.getCookie();
|
||||
if (cookies) {
|
||||
headers.set('Cookie', cookies);
|
||||
}
|
||||
return headers;
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
queryClient,
|
||||
});
|
||||
|
||||
export type { RouterInputs, RouterOutputs } from '@acme/api';
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { expoClient } from '@better-auth/expo/client';
|
||||
import { createAuthClient } from 'better-auth/react';
|
||||
|
||||
import { getBaseUrl } from './base-url';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: getBaseUrl(),
|
||||
plugins: [
|
||||
expoClient({
|
||||
scheme: 'expo',
|
||||
storagePrefix: 'expo',
|
||||
storage: SecureStore,
|
||||
}),
|
||||
],
|
||||
});
|
||||
26
apps/expo/src/utils/convex.ts
Normal file
26
apps/expo/src/utils/convex.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import Constants from 'expo-constants';
|
||||
import { ConvexReactClient } from 'convex/react';
|
||||
|
||||
const getConvexUrl = (): string => {
|
||||
// Allow override via Expo extra config (set in app.config.ts for production)
|
||||
const fromConfig = Constants.expoConfig?.extra?.convexUrl as
|
||||
| string
|
||||
| undefined;
|
||||
if (fromConfig) return fromConfig;
|
||||
|
||||
// Fall back to deriving from the dev server host (same pattern as getBaseUrl)
|
||||
const debuggerHost = Constants.expoConfig?.hostUri;
|
||||
const localhost = debuggerHost?.split(':')[0];
|
||||
|
||||
if (!localhost) {
|
||||
throw new Error(
|
||||
'Could not determine Convex URL. Set extra.convexUrl in app.config.ts for production.',
|
||||
);
|
||||
}
|
||||
|
||||
// Point at the self-hosted Convex backend on the local network
|
||||
// Update this port if your Convex backend runs on a different port
|
||||
return `http://${localhost}:3210`;
|
||||
};
|
||||
|
||||
export const convex = new ConvexReactClient(getConvexUrl());
|
||||
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ const Profile = async () => {
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card className='border-border/40'>
|
||||
<ProfileHeader preloadedUser={preloadedUser} />
|
||||
<ProfileHeader />
|
||||
<AvatarUpload preloadedUser={preloadedUser} />
|
||||
<Separator className='my-6' />
|
||||
<UserInfoForm
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022", "dom", "dom.iterable"],
|
||||
"jsx": "preserve",
|
||||
"types": ["node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
1
bun.lock
1
bun.lock
@@ -52,7 +52,6 @@
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-web": "~0.21.2",
|
||||
"react-native-worklets": "~0.5.2",
|
||||
"superjson": "2.2.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gib/eslint-config": "workspace:*",
|
||||
|
||||
10982
packages/ui/.cache/tsbuildinfo.json
Normal file
10982
packages/ui/.cache/tsbuildinfo.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ type EventType =
|
||||
| 'focusin'
|
||||
| 'focusout';
|
||||
|
||||
export const useOnClickOutside = <T,>(
|
||||
export const useOnClickOutside = <T extends Element>(
|
||||
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
|
||||
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
||||
eventType: EventType = 'mousedown',
|
||||
|
||||
Reference in New Issue
Block a user