From da3e511d4c6f29dfbce18736057a6e7b1bf21f77 Mon Sep 17 00:00:00 2001 From: Cloud User Date: Wed, 8 Jul 2026 21:50:45 +0300 Subject: [PATCH] feat(expo): mobile app poc --- core-mobile-fixes.md | 25 + docs/mobile-api-contract.md | 59 + expo-app/.env.example | 1 + expo-app/.gitignore | 9 + expo-app/README.md | 18 + expo-app/app.json | 51 + expo-app/app/(auth)/_layout.tsx | 10 + expo-app/app/(auth)/sign-in.tsx | 177 + expo-app/app/(auth)/sign-up.tsx | 195 + expo-app/app/(chat)/_layout.tsx | 5 + expo-app/app/(chat)/chat/[threadId].tsx | 148 + expo-app/app/(chat)/index.tsx | 209 + expo-app/app/(chat)/temporary.tsx | 136 + expo-app/app/_layout.tsx | 56 + expo-app/babel.config.js | 17 + expo-app/eas.json | 14 + expo-app/package.json | 54 + expo-app/pnpm-lock.yaml | 6680 ++++++++++++++++++++++ expo-app/src/components/Composer.tsx | 367 ++ expo-app/src/components/MessageList.tsx | 252 + expo-app/src/components/ModelPicker.tsx | 163 + expo-app/src/components/ThreadDrawer.tsx | 350 ++ expo-app/src/components/ui.tsx | 314 + expo-app/src/hooks/useAuthSession.ts | 12 + expo-app/src/hooks/useChatController.ts | 214 + expo-app/src/lib/api.ts | 225 + expo-app/src/lib/auth.ts | 18 + expo-app/src/lib/config.ts | 5 + expo-app/src/lib/messages.ts | 57 + expo-app/src/lib/stream.ts | 356 ++ expo-app/src/lib/types.ts | 64 + expo-app/src/store/chat-store.ts | 48 + expo-app/src/theme.ts | 138 + expo-app/tsconfig.json | 12 + mobile-expo-app-plan.md | 386 ++ package.json | 6 +- pnpm-lock.yaml | 30 + src/app/api/chat/message/[id]/route.ts | 41 + src/app/api/chat/regenerate/route.ts | 43 + src/app/api/mobile/auth/config/route.ts | 21 + src/app/api/thread/[id]/route.ts | 87 + src/lib/auth/auth-instance.ts | 2 + tsconfig.json | 2 +- 43 files changed, 11072 insertions(+), 5 deletions(-) create mode 100644 core-mobile-fixes.md create mode 100644 docs/mobile-api-contract.md create mode 100644 expo-app/.env.example create mode 100644 expo-app/.gitignore create mode 100644 expo-app/README.md create mode 100644 expo-app/app.json create mode 100644 expo-app/app/(auth)/_layout.tsx create mode 100644 expo-app/app/(auth)/sign-in.tsx create mode 100644 expo-app/app/(auth)/sign-up.tsx create mode 100644 expo-app/app/(chat)/_layout.tsx create mode 100644 expo-app/app/(chat)/chat/[threadId].tsx create mode 100644 expo-app/app/(chat)/index.tsx create mode 100644 expo-app/app/(chat)/temporary.tsx create mode 100644 expo-app/app/_layout.tsx create mode 100644 expo-app/babel.config.js create mode 100644 expo-app/eas.json create mode 100644 expo-app/package.json create mode 100644 expo-app/pnpm-lock.yaml create mode 100644 expo-app/src/components/Composer.tsx create mode 100644 expo-app/src/components/MessageList.tsx create mode 100644 expo-app/src/components/ModelPicker.tsx create mode 100644 expo-app/src/components/ThreadDrawer.tsx create mode 100644 expo-app/src/components/ui.tsx create mode 100644 expo-app/src/hooks/useAuthSession.ts create mode 100644 expo-app/src/hooks/useChatController.ts create mode 100644 expo-app/src/lib/api.ts create mode 100644 expo-app/src/lib/auth.ts create mode 100644 expo-app/src/lib/config.ts create mode 100644 expo-app/src/lib/messages.ts create mode 100644 expo-app/src/lib/stream.ts create mode 100644 expo-app/src/lib/types.ts create mode 100644 expo-app/src/store/chat-store.ts create mode 100644 expo-app/src/theme.ts create mode 100644 expo-app/tsconfig.json create mode 100644 mobile-expo-app-plan.md create mode 100644 src/app/api/chat/message/[id]/route.ts create mode 100644 src/app/api/chat/regenerate/route.ts create mode 100644 src/app/api/mobile/auth/config/route.ts create mode 100644 src/app/api/thread/[id]/route.ts diff --git a/core-mobile-fixes.md b/core-mobile-fixes.md new file mode 100644 index 000000000..8967098da --- /dev/null +++ b/core-mobile-fixes.md @@ -0,0 +1,25 @@ +# Shared Core Fixes Backlog + +These are non-blocking fixes that should benefit both the web app and mobile +app. Required mobile-enabling backend changes belong in this branch directly, +not in this backlog. + +## Permission Hardening + +- Audit chat server actions for authorization symmetry with HTTP routes. The new + mobile routes check thread/message ownership before mutation; equivalent server + actions should be reviewed so direct calls cannot delete or rename resources + outside the current user. +- Add focused tests around destructive chat operations: delete message, delete + thread, delete all threads, rename thread, and regenerate from message. + +## Error Contracts + +- Normalize API error response bodies across web and mobile routes. Some routes + return plain text responses while others return `{ error }` or `{ message }`. + A shared shape would simplify web UI handling and native clients. + +## Storage UX + +- Surface storage misconfiguration consistently in web flows before upload starts, + matching the structured details returned by `/api/storage/info`. diff --git a/docs/mobile-api-contract.md b/docs/mobile-api-contract.md new file mode 100644 index 000000000..df8c6f462 --- /dev/null +++ b/docs/mobile-api-contract.md @@ -0,0 +1,59 @@ +# Mobile API Contract + +This document records the backend contract used by `expo-app`. It is intentionally +HTTP-focused so native clients do not depend on Next.js server actions. + +## Auth + +- `GET /api/mobile/auth/config` + - Returns email/password availability, sign-up availability, first-user state, + enabled social providers, backend base URL, and auth base path. +- `GET/POST /api/auth/[...all]` + - Better Auth handler with Expo plugin support. + - Native clients use `@better-auth/expo/client` with SecureStore-backed cookie + persistence. + +## Threads + +- `GET /api/thread` + - Returns the signed-in user's thread list with `lastMessageAt`. +- `GET /api/thread/:id` + - Returns a thread plus messages after verifying ownership. +- `PATCH /api/thread/:id` + - Body: `{ "title": string }`. + - Renames a thread after verifying ownership. +- `DELETE /api/thread/:id` + - Deletes a thread after verifying ownership. + +## Chat + +- `POST /api/chat` + - Persistent UI message stream endpoint. + - Body includes `id`, `message`, `chatModel`, `toolChoice`, `mentions`, + `allowedMcpServers`, `allowedAppDefaultToolkit`, `imageTool`, and + `attachments`. +- `POST /api/chat/temporary` + - Temporary UI message stream endpoint. + - Body: `{ "messages": UIMessage[], "chatModel"?: ChatModel, + "instructions"?: string }`. +- `POST /api/chat/title` + - Streams a generated title and upserts it into the thread. +- `DELETE /api/chat/message/:id` + - Deletes a message after verifying ownership through its thread. +- `POST /api/chat/regenerate` + - Body: `{ "messageId": string }`. + - Deletes the target message and later messages in the same thread after + verifying ownership. +- `GET /api/chat/models` + - Returns enabled provider/model metadata. + +## Storage + +- `POST /api/storage/upload` + - Multipart fallback upload endpoint. Field name: `file`. + +## Advanced Capabilities + +MCP, agents, workflows, default tools, image generation, storage, rate limits, +and provider execution remain server-owned. Mobile clients pass selection state +through the chat request body and render returned `UIMessage.parts`. diff --git a/expo-app/.env.example b/expo-app/.env.example new file mode 100644 index 000000000..ec4d32e45 --- /dev/null +++ b/expo-app/.env.example @@ -0,0 +1 @@ +EXPO_PUBLIC_BACKEND_URL=http://localhost:3000 diff --git a/expo-app/.gitignore b/expo-app/.gitignore new file mode 100644 index 000000000..d57ace9e9 --- /dev/null +++ b/expo-app/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.expo/ +dist/ + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli diff --git a/expo-app/README.md b/expo-app/README.md new file mode 100644 index 000000000..b038889f1 --- /dev/null +++ b/expo-app/README.md @@ -0,0 +1,18 @@ +# Better Chatbot Expo App + +Native iOS and Android client for the Better Chatbot backend in this repository. + +## Setup + +1. Copy `.env.example` to `.env`. +2. Set `EXPO_PUBLIC_BACKEND_URL` to the reachable Next.js backend URL. + - iOS simulator can usually use `http://localhost:3000`. + - Android emulator usually needs `http://10.0.2.2:3000`. + - Physical devices need a LAN or tunneled URL. +3. Run `pnpm install` inside `expo-app`. +4. Start the backend with `NO_HTTPS=1 pnpm dev` from the repository root. +5. Start mobile with `pnpm start` inside `expo-app`. + +The app uses Better Auth Expo session storage, native auth screens, persistent +chat, temporary chat, model selection, thread list actions, message actions, +and file/image attachment upload through the existing backend storage APIs. diff --git a/expo-app/app.json b/expo-app/app.json new file mode 100644 index 000000000..82be9242d --- /dev/null +++ b/expo-app/app.json @@ -0,0 +1,51 @@ +{ + "expo": { + "name": "Better Chatbot", + "slug": "better-chatbot", + "scheme": "betterchatbot", + "version": "0.1.0", + "orientation": "portrait", + "userInterfaceStyle": "automatic", + "ios": { + "supportsTablet": true, + "bundleIdentifier": "app.betterchatbot.mobile", + "infoPlist": { + "ITSAppUsesNonExemptEncryption": false + } + }, + "android": { + "package": "app.betterchatbot.mobile", + "adaptiveIcon": { + "backgroundColor": "#0a0a0a" + } + }, + "plugins": [ + "expo-router", + "expo-secure-store", + "expo-web-browser", + [ + "expo-document-picker", + { + "iCloudContainerEnvironment": "Development" + } + ], + [ + "expo-image-picker", + { + "photosPermission": "Allow Better Chatbot to attach images from your photo library.", + "microphonePermission": false + } + ], + "expo-font" + ], + "experiments": { + "typedRoutes": true + }, + "extra": { + "router": {}, + "eas": { + "projectId": "f4d9cc53-89d8-45a2-bbb0-cf14c690edd5" + } + } + } +} diff --git a/expo-app/app/(auth)/_layout.tsx b/expo-app/app/(auth)/_layout.tsx new file mode 100644 index 000000000..154790550 --- /dev/null +++ b/expo-app/app/(auth)/_layout.tsx @@ -0,0 +1,10 @@ +import { Stack } from "expo-router"; + +export default function AuthLayout() { + return ( + + + + + ); +} diff --git a/expo-app/app/(auth)/sign-in.tsx b/expo-app/app/(auth)/sign-in.tsx new file mode 100644 index 000000000..7b25ff61d --- /dev/null +++ b/expo-app/app/(auth)/sign-in.tsx @@ -0,0 +1,177 @@ +import { useQuery } from "@tanstack/react-query"; +import * as Linking from "expo-linking"; +import { Link, router } from "expo-router"; +import { Github, Mail } from "lucide-react-native"; +import { useState } from "react"; +import { + KeyboardAvoidingView, + Platform, + StyleSheet, + Text, + View, +} from "react-native"; +import { ErrorBanner, Button, Field, Screen } from "@/components/ui"; +import { getAuthConfig } from "@/lib/api"; +import { authClient } from "@/lib/auth"; +import { colors, spacing, typography } from "@/theme"; + +export default function SignInScreen() { + const config = useQuery({ + queryKey: ["auth-config"], + queryFn: getAuthConfig, + }); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + async function signInEmail() { + setLoading(true); + setError(null); + try { + const result = await authClient.signIn.email({ email, password }); + if (result?.error) { + throw new Error(result.error.message || "Could not sign in"); + } + router.replace("/"); + } catch (err: any) { + setError(err.message || "Could not sign in"); + } finally { + setLoading(false); + } + } + + async function signInSocial(provider: "github" | "google" | "microsoft") { + setLoading(true); + setError(null); + try { + const result = await authClient.signIn.social({ + provider, + callbackURL: Linking.createURL("/"), + }); + if (result?.error) { + throw new Error( + result.error.message || "Could not start social sign in", + ); + } + } catch (err: any) { + setError(err.message || "Could not start social sign in"); + } finally { + setLoading(false); + } + } + + const providers = config.data?.socialProviders; + + return ( + + + + Better Chatbot + Sign in to continue your chats. + + + {config.data?.emailAndPasswordEnabled !== false ? ( + + + +