Skip to content
Open

Mobile #1113

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6b765e8
feat(mobile-pairing): add QR-based device pairing module
Ashraf-Ali-aa Jun 19, 2026
763dc50
feat(mobile-pairing): add IPC handlers and preload bridge
Ashraf-Ali-aa Jun 19, 2026
90ceec4
feat(web-server): support mobile device token authentication
Ashraf-Ali-aa Jun 19, 2026
cabd655
feat(settings): add mobile devices pairing UI
Ashraf-Ali-aa Jun 19, 2026
7074112
refactor(offline-queue): abstract storage for cross-platform support
Ashraf-Ali-aa Jun 19, 2026
a8f7937
test(web-server): add mobile token authentication test coverage
Ashraf-Ali-aa Jun 19, 2026
cce1697
chore(ci): add mobile app pipeline with path-based triggering
Ashraf-Ali-aa Jun 19, 2026
d2e2a27
feat(mobile): scaffold Expo React Native companion app
Ashraf-Ali-aa Jun 19, 2026
bdddf45
chore(mobile): exclude Expo generated/native artifacts from prettier
Ashraf-Ali-aa Jun 20, 2026
b71347d
fix CR
Ashraf-Ali-aa Jun 21, 2026
b5f103a
fix(mobile): address CodeRabbit review feedback on mobile PR
Ashraf-Ali-aa Jun 21, 2026
60120f0
ci(mobile): pin GitHub Actions to immutable commit SHAs
Ashraf-Ali-aa Jun 21, 2026
18f47f0
fix(mobile): address Codex review feedback on mobile PR
Ashraf-Ali-aa Jun 27, 2026
6b60aef
fix(mobile): address Codex review feedback (round 2)
Ashraf-Ali-aa Jun 27, 2026
220b67c
fix(mobile): address Codex review feedback (round 3)
Ashraf-Ali-aa Jun 28, 2026
94f43a7
Merge upstream/rc into mobile
Ashraf-Ali-aa Jun 28, 2026
95667c8
fix(mobile): address Codex review feedback (round 4)
Ashraf-Ali-aa Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 56 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ on:
push:
branches: [main, rc]

permissions:
contents: read
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Grant pull request read access to path filtering

This workflow runs on pull_request, and dorny/paths-filter uses the GitHub REST API for PR diffs and requires pull-requests: read. Because the new top-level permissions only grant contents: read, the changes job can fail before mobile checks are even selected; add pull-requests: read at the workflow or changes job level.

Useful? React with 👍 / 👎.


jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
Expand All @@ -23,10 +28,57 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
- run: npm ci
- run: npm run test

# Detect if mobile/shared code changed to conditionally run mobile-checks
changes:
runs-on: ubuntu-latest
outputs:
mobile: ${{ steps.filter.outputs.mobile }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: dorny/paths-filter@d1c1ffe0248fe513906c8e24db8ea791d46f8590 # v3.0.3
id: filter
with:
filters: |
mobile:
- 'apps/mobile/**'
- 'src/shared/**'
- 'src/web/hooks/**'

# Mobile checks job - runs only when mobile or shared code changes
mobile-checks:
needs: changes
if: needs.changes.outputs.mobile == 'true'
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
defaults:
run:
working-directory: apps/mobile
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: apps/mobile/package-lock.json
- run: npm ci
- name: TypeScript type check
run: npx tsc --noEmit
- name: ESLint
run: npx eslint .
- name: Jest tests
run: npm test
- name: Expo doctor
run: npx expo-doctor
7 changes: 7 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ AGENTS.md

# Auto-generated by release tooling — see CLAUDE.md "Do Not Edit: docs/releases.md"
docs/releases.md

# Mobile (Expo) generated/native build artifacts
apps/mobile/.expo/
apps/mobile/ios/
apps/mobile/android/
apps/mobile/expo-env.d.ts
apps/mobile/uniwind-types.d.ts
298 changes: 298 additions & 0 deletions apps/mobile/.agents/skills/building-native-ui/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
---
name: building-native-ui
description: Complete guide for building beautiful apps with Expo Router. Covers fundamentals, styling, components, navigation, animations, patterns, and native tabs.
version: 1.0.1
license: MIT
---

# Expo UI Guidelines

## References

Consult these resources as needed:

```text
references/
animations.md Reanimated: entering, exiting, layout, scroll-driven, gestures
controls.md Native iOS: Switch, Slider, SegmentedControl, DateTimePicker, Picker
form-sheet.md Form sheets in expo-router: configuration, footers and background interaction.
gradients.md CSS gradients via experimental_backgroundImage (New Arch only)
icons.md SF Symbols via expo-image (sf: source), names, animations, weights
media.md Camera, audio, video, and file saving
route-structure.md Route conventions, dynamic routes, groups, folder organization
search.md Search bar with headers, useSearch hook, filtering patterns
storage.md SQLite, AsyncStorage, SecureStore
tabs.md NativeTabs, migration from JS tabs, iOS 26 features
toolbar-and-headers.md Stack headers and toolbar buttons, menus, search (iOS only)
visual-effects.md Blur (expo-blur) and liquid glass (expo-glass-effect)
webgpu-three.md 3D graphics, games, GPU visualizations with WebGPU and Three.js
zoom-transitions.md Apple Zoom: fluid zoom transitions with Link.AppleZoom (iOS 18+)
```

## Running the App

**CRITICAL: This app requires a custom Expo development build and will not work in Expo Go.**

Do not attempt to run this app with the Expo Go client - it relies on native modules and configuration that Expo Go does not ship with. Use a custom dev build instead:

1. **iOS**: `npx expo run:ios` (or `npx serve-sim` for the simulator verification flow used in this repo)
2. **Android**: `npx expo run:android`
3. **Web**: `npx agent-browser`

### Why a Custom Build Is Required

This project pulls in capabilities Expo Go does not support, including:

- **Local Expo modules** (custom native code in `modules/`)
- **Apple targets** (widgets, app clips, extensions via `@bacons/apple-targets`)
- **Third-party native modules** not bundled with Expo Go
- **Custom native configuration** that can't be expressed in `app.json` alone

If a build fails, fix the native config or run `npx expo prebuild` - do not fall back to Expo Go.

## Code Style

- Be cautious of unterminated strings. Ensure nested backticks are escaped; never forget to escape quotes correctly.
- Always use import statements at the top of the file.
- Always use kebab-case for file names, e.g. `comment-card.tsx`
- Always remove old route files when moving or restructuring navigation
- Never use special characters in file names
- Configure tsconfig.json with path aliases, and prefer aliases over relative imports for refactors.

## Routes

See `./references/route-structure.md` for detailed route conventions.

- Routes belong in the `app` directory.
- Never co-locate components, types, or utilities in the app directory. This is an anti-pattern.
- Ensure the app always has a route that matches "/", it may be inside a group route.

## Library Preferences

- Never use modules removed from React Native such as Picker, WebView, SafeAreaView, or AsyncStorage
- Never use legacy expo-permissions
- `expo-audio` not `expo-av`
- `expo-video` not `expo-av`
- `expo-image` with `source="sf:name"` for SF Symbols, not `expo-symbols` or `@expo/vector-icons`
- `react-native-safe-area-context` not react-native SafeAreaView
- `process.env.EXPO_OS` not `Platform.OS`
- `React.use` not `React.useContext`
- `expo-image` Image component instead of intrinsic element `img`
- `expo-glass-effect` for liquid glass backdrops

## Responsiveness

- Always wrap root component in a scroll view for responsiveness
- Use `<ScrollView contentInsetAdjustmentBehavior="automatic" />` instead of `<SafeAreaView>` for smarter safe area insets
- `contentInsetAdjustmentBehavior="automatic"` should be applied to FlatList and SectionList as well
- Use flexbox instead of Dimensions API
- ALWAYS prefer `useWindowDimensions` over `Dimensions.get()` to measure screen size

## Behavior

- Use expo-haptics conditionally on iOS to make more delightful experiences
- Use views with built-in haptics like `<Switch />` from React Native and `@react-native-community/datetimepicker`
- When a route belongs to a Stack, its first child should almost always be a ScrollView with `contentInsetAdjustmentBehavior="automatic"` set
- When adding a `ScrollView` to the page it should almost always be the first component inside the route component
- Prefer `headerSearchBarOptions` in Stack.Screen options to add a search bar
- Use the `<Text selectable />` prop on text containing data that could be copied
- Consider formatting large numbers like 1.4M or 38k
- Never use intrinsic elements like 'img' or 'div' unless in a webview or Expo DOM component

# Styling

Follow Apple Human Interface Guidelines.

## General Styling Rules

- Prefer flex gap over margin and padding styles
- Prefer padding over margin where possible
- Always account for safe area, either with stack headers, tabs, or ScrollView/FlatList `contentInsetAdjustmentBehavior="automatic"`
- Ensure both top and bottom safe area insets are accounted for
- Inline styles not StyleSheet.create unless reusing styles is faster
- Add entering and exiting animations for state changes
- Use `{ borderCurve: 'continuous' }` for rounded corners unless creating a capsule shape
- ALWAYS use a navigation stack title instead of a custom text element on the page
- When padding a ScrollView, use `contentContainerStyle` padding and gap instead of padding on the ScrollView itself (reduces clipping)
- CSS and Tailwind are not supported - use inline styles

## Text Styling

- Add the `selectable` prop to every `<Text/>` element displaying important data or error messages
- Counters should use `{ fontVariant: 'tabular-nums' }` for alignment

## Shadows

Use CSS `boxShadow` style prop. NEVER use legacy React Native shadow or elevation styles.

```tsx
<View style={{ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.05)' }} />
```

'inset' shadows are supported.

# Navigation

## Link

Use `<Link href="/path" />` from 'expo-router' for navigation between routes.

```tsx
import { Link } from 'expo-router';

// Basic link
<Link href="/path" />

// Wrapping custom components
<Link href="/path" asChild>
<Pressable>...</Pressable>
</Link>
```

Whenever possible, include a `<Link.Preview>` to follow iOS conventions. Add context menus and previews frequently to enhance navigation.

## Stack

- ALWAYS use `_layout.tsx` files to define stacks
- Use Stack from 'expo-router/stack' for native navigation stacks

### Page Title

Set the page title in Stack.Screen options:

```tsx
<Stack.Screen options={{ title: 'Home' }} />
```

## Context Menus

Add long press context menus to Link components:

```tsx
import { Link } from 'expo-router';

<Link href="/settings" asChild>
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Menu>
<Link.MenuAction title="Share" icon="square.and.arrow.up" onPress={handleSharePress} />
<Link.MenuAction title="Block" icon="nosign" destructive onPress={handleBlockPress} />
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction title="Copy" icon="doc.on.doc" onPress={() => {}} />
<Link.MenuAction title="Delete" icon="trash" destructive onPress={() => {}} />
</Link.Menu>
</Link.Menu>
</Link>;
```

## Link Previews

Use link previews frequently to enhance navigation:

```tsx
<Link href="/settings">
<Link.Trigger>
<Pressable>
<Card />
</Pressable>
</Link.Trigger>
<Link.Preview />
</Link>
```

Link preview can be used with context menus.

## Modal

Present a screen as a modal:

```tsx
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
```

Prefer this to building a custom modal component.

## Sheet

Present a screen as a dynamic form sheet:

```tsx
<Stack.Screen
name="sheet"
options={{
presentation: 'formSheet',
sheetGrabberVisible: true,
sheetAllowedDetents: [0.5, 1.0],
contentStyle: { backgroundColor: 'transparent' },
}}
/>
```

- Using `contentStyle: { backgroundColor: "transparent" }` makes the background liquid glass on iOS 26+.

## Common route structure

A standard app layout with tabs and stacks inside each tab:

```text
app/
_layout.tsx - <NativeTabs />
(index,search)/
_layout.tsx - <Stack />
index.tsx - Main list
search.tsx - Search view
```

```tsx
// app/_layout.tsx
import { NativeTabs, Icon, Label } from 'expo-router/unstable-native-tabs';
import { Theme } from '../components/theme';

export default function Layout() {
return (
<Theme>
<NativeTabs>
<NativeTabs.Trigger name="(index)">
<Icon sf="list.dash" />
<Label>Items</Label>
</NativeTabs.Trigger>
<NativeTabs.Trigger name="(search)" role="search" />
</NativeTabs>
</Theme>
);
}
```

Create a shared group route so both tabs can push common screens:

```tsx
// app/(index,search)/_layout.tsx
import { Stack } from 'expo-router/stack';
import { PlatformColor } from 'react-native';

export default function Layout({ segment }) {
const screen = segment.match(/\((.*)\)/)?.[1]!;
const titles: Record<string, string> = { index: 'Items', search: 'Search' };

return (
<Stack
screenOptions={{
headerTransparent: true,
headerShadowVisible: false,
headerLargeTitleShadowVisible: false,
headerLargeStyle: { backgroundColor: 'transparent' },
headerTitleStyle: { color: PlatformColor('label') },
headerLargeTitle: true,
headerBlurEffect: 'none',
headerBackButtonDisplayMode: 'minimal',
}}
>
<Stack.Screen name={screen} options={{ title: titles[screen] }} />
<Stack.Screen name="i/[id]" options={{ headerLargeTitle: false }} />
</Stack>
);
}
```
Loading