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:
SubashMohanandClaude Opus 4.8 authored and GitHub committed 2026-06-09 04:38:12 +00:00
1 parent 42a274e101
commit 7e3f1d7cda
18 files changed
+1772

No files matched your search

+43
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
+66
View File
@@ -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.
+44
View File
@@ -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

+12
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+8
View File
@@ -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" });
+4
View File
@@ -0,0 +1,4 @@
/// <reference types="nativewind/types" />
// Allow side-effect imports of the Tailwind entry stylesheet.
declare module "*.css";
+50
View File
@@ -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
}
+39
View File
@@ -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>
);
}
+16
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+21
View File
@@ -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
+21
View File
@@ -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);
},
};
}
+6
View File
@@ -0,0 +1,6 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{ts,tsx}"],
presets: [require("nativewind/preset")],
theme: { extend: {} },
};
+21
View File
@@ -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"
]
}