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.
|
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
|
The broken T3 Turbo template code has been cleaned up — all `@acme/*` references,
|
||||||
code and has not been migrated to use Convex or `@convex-dev/auth`.
|
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,
|
### Current state
|
||||||
be aware that the following are all broken and will need to be addressed:
|
|
||||||
|
|
||||||
| File | Issue |
|
The app is wired up with the correct providers and patterns:
|
||||||
| ----------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `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 |
|
|
||||||
|
|
||||||
When the time comes to properly implement the Expo app, the right approach is to start
|
- **`src/app/_layout.tsx`** — `ConvexAuthProvider` wraps the root, initialized with
|
||||||
fresh with Convex + `@convex-dev/auth` patterns (mirroring how the Next.js app works),
|
a `ConvexReactClient` pointed at the self-hosted backend
|
||||||
not to try to incrementally fix the broken T3 Turbo code.
|
- **`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
|
**2. Root `.dockerignore` includes `.env` in build context** ℹ️ Known, works intentionally
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
The root `.dockerignore` has `.env` commented out, meaning the `.env` file is sent to
|
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_*`)
|
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
|
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.
|
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
|
`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
|
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:
|
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.
|
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.
|
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
|
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:
|
Currently:
|
||||||
|
|
||||||
@@ -1351,14 +1355,7 @@ moving everything to `docker/scripts/`.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**8. Expo `eas.json` specifies pnpm** ⚠️ Low priority (Expo is WIP anyway)
|
**7. React Email templates not yet implemented** ℹ️ Future enhancement
|
||||||
|
|
||||||
`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
|
|
||||||
|
|
||||||
`@react-email/components` and `react-email` are in `packages/backend/package.json` as
|
`@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
|
planned dependencies. The current `usesend.ts` uses inline HTML strings for email
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"base": {
|
"base": {
|
||||||
"node": "22.12.0",
|
"node": "22.12.0",
|
||||||
"pnpm": "9.15.4",
|
"bun": "1.3.10",
|
||||||
"ios": {
|
"ios": {
|
||||||
"resourceClass": "m-medium"
|
"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 { defineConfig } from 'eslint/config';
|
||||||
|
|
||||||
|
import { baseConfig } from '@gib/eslint-config/base';
|
||||||
|
import { reactConfig } from '@gib/eslint-config/react';
|
||||||
|
|
||||||
export default defineConfig(
|
export default defineConfig(
|
||||||
{
|
{
|
||||||
ignores: ['.expo/**', 'expo-plugins/**'],
|
ignores: ['.expo/**', 'expo-plugins/**'],
|
||||||
|
|||||||
@@ -49,8 +49,7 @@
|
|||||||
"react-native-safe-area-context": "~5.6.2",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.2",
|
"react-native-web": "~0.21.2",
|
||||||
"react-native-worklets": "~0.5.2",
|
"react-native-worklets": "~0.5.2"
|
||||||
"superjson": "2.2.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gib/eslint-config": "workspace:*",
|
"@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 { useColorScheme } from 'react-native';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
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';
|
import '../styles.css';
|
||||||
|
|
||||||
// This is the main layout of the app
|
const RootLayout = () => {
|
||||||
// It wraps your pages with the providers they need
|
|
||||||
export default function RootLayout() {
|
|
||||||
const colorScheme = useColorScheme();
|
const colorScheme = useColorScheme();
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<ConvexAuthProvider client={convex}>
|
||||||
{/*
|
|
||||||
The Stack component displays the current page.
|
|
||||||
It also allows you to configure your screens
|
|
||||||
*/}
|
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerStyle: {
|
headerStyle: {
|
||||||
backgroundColor: '#c03484',
|
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||||
},
|
},
|
||||||
|
headerTintColor: colorScheme === 'dark' ? '#fafaf9' : '#1c1917',
|
||||||
contentStyle: {
|
contentStyle: {
|
||||||
backgroundColor: colorScheme == 'dark' ? '#09090B' : '#FFFFFF',
|
backgroundColor: colorScheme === 'dark' ? '#1c1917' : '#faf9f7',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<StatusBar />
|
<StatusBar style='auto' />
|
||||||
</QueryClientProvider>
|
</ConvexAuthProvider>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default RootLayout;
|
||||||
|
|||||||
@@ -1,172 +1,54 @@
|
|||||||
import { useState } from 'react';
|
import { Pressable, Text, View } from 'react-native';
|
||||||
import { Pressable, Text, TextInput, View } from 'react-native';
|
|
||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { Link, Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import { LegendList } from '@legendapp/list';
|
import { useAuthActions } from '@convex-dev/auth/react';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useConvexAuth, useQuery } from 'convex/react';
|
||||||
|
|
||||||
import type { RouterOutputs } from '~/utils/api';
|
import { api } from '@gib/backend/convex/_generated/api.js';
|
||||||
import { trpc } from '~/utils/api';
|
|
||||||
import { authClient } from '~/utils/auth';
|
|
||||||
|
|
||||||
function PostCard(props: {
|
const Index = () => {
|
||||||
post: RouterOutputs['post']['all'][number];
|
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||||
onDelete: () => void;
|
const { signOut } = useAuthActions();
|
||||||
}) {
|
const user = useQuery(api.auth.getUser, isAuthenticated ? {} : 'skip');
|
||||||
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());
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className='mt-4 flex gap-2'>
|
<SafeAreaView className='bg-background flex-1'>
|
||||||
<TextInput
|
<Stack.Screen options={{ title: 'Home' }} />
|
||||||
className='border-input bg-background text-foreground items-center rounded-md border px-3 text-lg leading-tight'
|
<View className='flex-1 items-center justify-center gap-4 p-6'>
|
||||||
value={title}
|
<Text className='text-foreground text-4xl font-bold'>
|
||||||
onChangeText={setTitle}
|
Convex Monorepo
|
||||||
placeholder='Title'
|
|
||||||
/>
|
|
||||||
{error?.data?.zodError?.fieldErrors.title && (
|
|
||||||
<Text className='text-destructive mb-2'>
|
|
||||||
{error.data.zodError.fieldErrors.title}
|
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
<Text className='text-muted-foreground text-center text-base'>
|
||||||
<TextInput
|
Your self-hosted Expo + Convex starter
|
||||||
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>
|
</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() {
|
{isLoading ? (
|
||||||
const { data: session } = authClient.useSession();
|
<Text className='text-muted-foreground'>Loading...</Text>
|
||||||
|
) : isAuthenticated ? (
|
||||||
return (
|
<View className='w-full gap-3'>
|
||||||
<>
|
<Text className='text-foreground text-center text-lg'>
|
||||||
<Text className='text-foreground pb-2 text-center text-xl font-semibold'>
|
Welcome{user?.name ? `, ${user.name}` : ''}!
|
||||||
{session?.user.name ? `Hello, ${session.user.name}` : 'Not logged in'}
|
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() =>
|
className='bg-primary items-center rounded-md p-3'
|
||||||
session
|
onPress={() => void signOut()}
|
||||||
? authClient.signOut()
|
|
||||||
: authClient.signIn.social({
|
|
||||||
provider: 'discord',
|
|
||||||
callbackURL: '/',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
className='bg-primary flex items-center rounded-sm p-2'
|
|
||||||
>
|
>
|
||||||
<Text>{session ? 'Sign Out' : 'Sign In With Discord'}</Text>
|
<Text className='text-primary-foreground font-semibold'>
|
||||||
|
Sign Out
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</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>
|
</View>
|
||||||
|
) : (
|
||||||
<LegendList
|
<View className='w-full gap-3'>
|
||||||
data={postQuery.data ?? []}
|
<Text className='text-muted-foreground text-center'>
|
||||||
estimatedItemSize={20}
|
Sign in to get started
|
||||||
keyExtractor={(item) => item.id}
|
</Text>
|
||||||
ItemSeparatorComponent={() => <View className='h-2' />}
|
{/* Add sign-in UI here — see apps/next/src/app/(auth)/sign-in for patterns */}
|
||||||
renderItem={(p) => (
|
</View>
|
||||||
<PostCard
|
|
||||||
post={p.item}
|
|
||||||
onDelete={() => deletePostMutation.mutate(p.item.id)}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
|
|
||||||
<CreatePost />
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|||||||
@@ -1,24 +1,21 @@
|
|||||||
import { SafeAreaView, Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import { Stack, useGlobalSearchParams } from 'expo-router';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { Stack, useLocalSearchParams } from 'expo-router';
|
||||||
|
|
||||||
import { trpc } from '~/utils/api';
|
const Post = () => {
|
||||||
|
const { id } = useLocalSearchParams<{ id: string }>();
|
||||||
export default function Post() {
|
|
||||||
const { id } = useGlobalSearchParams<{ id: string }>();
|
|
||||||
const { data } = useQuery(trpc.post.byId.queryOptions({ id }));
|
|
||||||
|
|
||||||
if (!data) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className='bg-background'>
|
<SafeAreaView className='bg-background flex-1'>
|
||||||
<Stack.Screen options={{ title: data.title }} />
|
<Stack.Screen options={{ title: 'Post' }} />
|
||||||
<View className='h-full w-full p-4'>
|
<View className='flex-1 p-4'>
|
||||||
<Text className='text-primary py-2 text-3xl font-bold'>
|
<Text className='text-foreground text-2xl font-bold'>Post {id}</Text>
|
||||||
{data.title}
|
<Text className='text-muted-foreground mt-2'>
|
||||||
|
Implement your post detail screen here using Convex queries.
|
||||||
</Text>
|
</Text>
|
||||||
<Text className='text-foreground py-4'>{data.content}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default Post;
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import 'nativewind/theme';
|
@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 */}
|
{/* Profile Card */}
|
||||||
<Card className='border-border/40'>
|
<Card className='border-border/40'>
|
||||||
<ProfileHeader preloadedUser={preloadedUser} />
|
<ProfileHeader />
|
||||||
<AvatarUpload preloadedUser={preloadedUser} />
|
<AvatarUpload preloadedUser={preloadedUser} />
|
||||||
<Separator className='my-6' />
|
<Separator className='my-6' />
|
||||||
<UserInfoForm
|
<UserInfoForm
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["ES2022", "dom", "dom.iterable"],
|
"lib": ["ES2022", "dom", "dom.iterable"],
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"types": ["node"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
|
|||||||
1
bun.lock
1
bun.lock
@@ -52,7 +52,6 @@
|
|||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.2",
|
"react-native-web": "~0.21.2",
|
||||||
"react-native-worklets": "~0.5.2",
|
"react-native-worklets": "~0.5.2",
|
||||||
"superjson": "2.2.3",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gib/eslint-config": "workspace:*",
|
"@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'
|
| 'focusin'
|
||||||
| 'focusout';
|
| 'focusout';
|
||||||
|
|
||||||
export const useOnClickOutside = <T,>(
|
export const useOnClickOutside = <T extends Element>(
|
||||||
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
|
ref: React.RefObject<T | null> | React.RefObject<T | null>[],
|
||||||
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
handler: (event: MouseEvent | TouchEvent | FocusEvent) => void,
|
||||||
eventType: EventType = 'mousedown',
|
eventType: EventType = 'mousedown',
|
||||||
|
|||||||
Reference in New Issue
Block a user