mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
feat(mobile): scaffold Onyx Expo (SDK 56) React Native app (#11800)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
42a274e101
commit
7e3f1d7cda
18 files changed
+1772
No files matched your search
@@ -0,0 +1,43 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# Native
|
||||
.kotlin/
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
|
||||
example
|
||||
|
||||
# generated native folders
|
||||
/ios
|
||||
/android
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{ "recommendations": ["expo.vscode-expo-tools"] }
|
||||
@@ -0,0 +1,66 @@
|
||||
# Onyx Mobile (Expo)
|
||||
|
||||
A standalone React Native app for Onyx, under `mobile/` — independent of `web/` (its own
|
||||
dependencies, lockfile, and tooling). Scaffolded with `create-expo-app` (Expo Router template).
|
||||
|
||||
## Stack
|
||||
|
||||
- **Expo SDK 56** (managed, New Architecture) · **Expo Router** · React 19.2 / RN 0.85 · **Bun**.
|
||||
- **NativeWind v4** for styling.
|
||||
> No design system wired yet — tokens/theme will be imported from the `@onyx-ai/shared`
|
||||
> package later (see the `Subash-Mohan/shared-package-mobile-web` branch).
|
||||
- **TanStack Query** (MMKV-persisted) · **Zustand** · **react-native-mmkv** · **@shopify/flash-list**.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node LTS on PATH, **Bun**, Xcode + an iOS simulator, CocoaPods.
|
||||
- Recommended: `brew install watchman` (faster, more reliable Metro file watching).
|
||||
- The project path must contain **no spaces** (RN/Xcode build scripts break otherwise).
|
||||
- Runs as a **development build** (not Expo Go) — `react-native-mmkv` v4 and FlashList are native
|
||||
modules absent from Expo Go.
|
||||
|
||||
## Setup (from inside `mobile/`)
|
||||
|
||||
```bash
|
||||
bun install
|
||||
bunx expo install --fix # reconcile any deps to SDK 56
|
||||
```
|
||||
|
||||
## Run (iOS simulator)
|
||||
|
||||
```bash
|
||||
bun run prebuild -- -p ios # or: bunx expo prebuild --clean -p ios (generates ios/)
|
||||
bun run run:ios # or: bunx expo run:ios --port 8082
|
||||
```
|
||||
|
||||
> The first iOS build compiles React Native **from source (~15 min)** — RN 0.85 has no prebuilt
|
||||
> CocoaPods artifact; this is expected, not a hang. Subsequent builds are incremental.
|
||||
> Metro runs on **port 8082** (8081 is commonly taken by Docker). Android: `bun run run:android`.
|
||||
|
||||
## Scripts
|
||||
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `bun run start` / `ios` / `android` / `web` | Metro / dev menu (port 8082). |
|
||||
| `bun run run:ios` / `run:android` | Build + launch the native dev build. |
|
||||
| `bun run prebuild` | Regenerate the native `ios/` + `android/` projects (CNG). |
|
||||
| `bun run typecheck` | `tsc --noEmit`. |
|
||||
| `bun run lint` | `expo lint`. |
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
src/app/ Expo Router routes (_layout.tsx provider stack, index.tsx placeholder home)
|
||||
src/query/ TanStack Query client + MMKV persister
|
||||
src/state/ MMKV instances + storage adapter (shared by Query + zustand persist)
|
||||
src/global.css Tailwind entry stylesheet (NativeWind)
|
||||
```
|
||||
|
||||
## Design system
|
||||
|
||||
Not wired yet. Tokens, theme, and shared utilities will be imported from the `@onyx-ai/shared`
|
||||
package once it lands (see the `Subash-Mohan/shared-package-mobile-web` branch). Nothing is
|
||||
vendored locally — the package owns the Style Dictionary build and the design tokens.
|
||||
|
||||
> The splash logo (`assets/images/splash-icon.png`) is a placeholder on the Onyx-blue background
|
||||
> (`#208AEF`) — replace it with the real Onyx logo art.
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Onyx",
|
||||
"slug": "mobile",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "onyx",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"ios": {
|
||||
"bundleIdentifier": "app.onyx.mobile",
|
||||
"supportsTablet": false
|
||||
},
|
||||
"android": {
|
||||
"package": "app.onyx.mobile",
|
||||
"predictiveBackGestureEnabled": false
|
||||
},
|
||||
"web": {
|
||||
"output": "static"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"backgroundColor": "#208AEF",
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"imageWidth": 120
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"ios": {
|
||||
"buildReactNativeFromSource": true
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 780 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,12 @@
|
||||
// NativeWind enables `className` on RN components: `jsxImportSource: "nativewind"`
|
||||
// rewrites JSX, and the "nativewind/babel" preset wires the CSS-interop transform.
|
||||
// babel-preset-expo also auto-adds the Reanimated/Worklets plugin when present.
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: [
|
||||
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||
"nativewind/babel",
|
||||
],
|
||||
};
|
||||
};
|
||||
+1417
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
// Default Expo Metro config wrapped with NativeWind so Tailwind classes compile
|
||||
// through the bundler. `input` points at the Tailwind entry stylesheet.
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
const config = getDefaultConfig(__dirname);
|
||||
|
||||
module.exports = withNativeWind(config, { input: "./src/global.css" });
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
/// <reference types="nativewind/types" />
|
||||
|
||||
// Allow side-effect imports of the Tailwind entry stylesheet.
|
||||
declare module "*.css";
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "mobile",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@shopify/flash-list": "2.0.2",
|
||||
"@tanstack/query-sync-storage-persister": "^5.101.0",
|
||||
"@tanstack/react-query": "^5.101.0",
|
||||
"@tanstack/react-query-persist-client": "^5.101.0",
|
||||
"expo": "~56.0.9",
|
||||
"expo-build-properties": "~56.0.17",
|
||||
"expo-constants": "~56.0.17",
|
||||
"expo-dev-client": "~56.0.19",
|
||||
"expo-font": "~56.0.5",
|
||||
"expo-linking": "~56.0.13",
|
||||
"expo-router": "~56.2.9",
|
||||
"expo-splash-screen": "~56.0.10",
|
||||
"expo-status-bar": "~56.0.4",
|
||||
"expo-system-ui": "~56.0.5",
|
||||
"nativewind": "^4.2.5",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-gesture-handler": "~2.31.1",
|
||||
"react-native-mmkv": "^4.3.1",
|
||||
"react-native-reanimated": "4.3.1",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "4.25.2",
|
||||
"react-native-web": "~0.21.0",
|
||||
"react-native-worklets": "0.8.3",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.2",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "~6.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "expo start --port 8082",
|
||||
"ios": "expo start --ios --port 8082",
|
||||
"android": "expo start --android --port 8082",
|
||||
"web": "expo start --web --port 8082",
|
||||
"run:ios": "expo run:ios --port 8082",
|
||||
"run:android": "expo run:android --port 8082",
|
||||
"prebuild": "expo prebuild --clean",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import "react-native-gesture-handler"; // must be first import
|
||||
import "../global.css";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { Stack } from "expo-router";
|
||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
import { StatusBar } from "expo-status-bar";
|
||||
import * as SplashScreen from "expo-splash-screen";
|
||||
|
||||
import { persister, persistMaxAge, queryClient } from "@/query/client";
|
||||
|
||||
// Show the native Onyx splash until the first frame is ready, then reveal the app.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
export default function RootLayout() {
|
||||
useEffect(() => {
|
||||
// No async init yet, so hide on the first render.
|
||||
// TODO(Subash-Mohan): once useFonts + @onyx-ai/shared init land, gate this
|
||||
// behind a readiness flag (return null until ready) so text never flashes in
|
||||
// the system font before custom fonts load.
|
||||
void SplashScreen.hideAsync();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<GestureHandlerRootView className="flex-1">
|
||||
<SafeAreaProvider>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{ persister, maxAge: persistMaxAge }}
|
||||
>
|
||||
<StatusBar style="auto" />
|
||||
<Stack screenOptions={{ headerShown: false }} />
|
||||
</PersistQueryClientProvider>
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Text, View } from "react-native";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
|
||||
// Placeholder home screen. Design system (tokens/theme) arrives via @onyx-ai/shared later.
|
||||
export default function Home() {
|
||||
return (
|
||||
<SafeAreaView className="flex-1 bg-white">
|
||||
<View className="flex-1 items-center justify-center gap-2 px-6">
|
||||
<Text className="text-2xl font-semibold text-neutral-900">Onyx Mobile</Text>
|
||||
<Text className="text-center text-base text-neutral-500">
|
||||
Scaffold ready — Expo SDK 56 · Expo Router · NativeWind v4.
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -0,0 +1,21 @@
|
||||
// TanStack Query client + MMKV-backed persister.
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||
|
||||
import { makeMmkvStorage, queryStorage } from "@/state/storage";
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000, // 30s — fresh enough to skip refetch storms
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false, // no-op on RN, explicit for parity
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const persister = createSyncStoragePersister({
|
||||
storage: makeMmkvStorage(queryStorage),
|
||||
});
|
||||
|
||||
export const persistMaxAge = 1000 * 60 * 60 * 24; // 24h
|
||||
@@ -0,0 +1,21 @@
|
||||
// MMKV instances + a string storage adapter shared by TanStack Query's persister
|
||||
// and (later) zustand's `persist` middleware.
|
||||
//
|
||||
// react-native-mmkv v4 has NO `new MMKV()` — use the createMMKV factory. Two
|
||||
// separate instances so a query-cache clear can't touch other persisted state.
|
||||
import { createMMKV, type MMKV } from "react-native-mmkv";
|
||||
|
||||
export const appStorage: MMKV = createMMKV({ id: "onyx.app" });
|
||||
export const queryStorage: MMKV = createMMKV({ id: "onyx.query-cache" });
|
||||
|
||||
export function makeMmkvStorage(mmkv: MMKV) {
|
||||
return {
|
||||
getItem: (name: string): string | null => mmkv.getString(name) ?? null,
|
||||
setItem: (name: string, value: string): void => {
|
||||
mmkv.set(name, value);
|
||||
},
|
||||
removeItem: (name: string): void => {
|
||||
mmkv.remove(name);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{ts,tsx}"],
|
||||
presets: [require("nativewind/preset")],
|
||||
theme: { extend: {} },
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@/assets/*": [
|
||||
"./assets/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts",
|
||||
"nativewind-env.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user