Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 9 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Workspace environment template.
# For mobile development, copy values into apps/mobile/.env.
# Workspace environment template (single source of truth).
# apps/mobile/.env should point to this file (symlink) in local development.

# Required for Google Places search and Android Maps key injection.
EXPO_PUBLIC_GOOGLE_PLACES_API_KEY=your_google_places_api_key
Expand All @@ -8,6 +8,13 @@ JAVA_HOME=/usr/lib/jvm/java-17-openjdk
# Optional: local backend used by mobile auth/sync in development.
EXPO_PUBLIC_BOOKMARKS_BACKEND_URL=http://127.0.0.1:8787

# Optional: Google OAuth client IDs (required for Google sign in).
# Create these in Google Cloud Console OAuth credentials.
EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=

# Optional: dev-only test auth controls.
EXPO_PUBLIC_BOOKMARKS_DEV_TEST_AUTH=1
EXPO_PUBLIC_BOOKMARKS_DEV_TEST_USER=dev-user
Expand Down
101 changes: 33 additions & 68 deletions .github/workflows/mobile-e2e-android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ on:
- preferences/**
- schema/**
- scripts/mobileE2eAndroid.sh
- scripts/mobileE2eAndroidShard.sh
- share/**
- sync/**
- package.json
Expand All @@ -37,6 +38,7 @@ on:
- preferences/**
- schema/**
- scripts/mobileE2eAndroid.sh
- scripts/mobileE2eAndroidShard.sh
- share/**
- sync/**
- package.json
Expand All @@ -48,14 +50,20 @@ concurrency:
cancel-in-progress: true

jobs:
maestro-android:
name: Maestro Android e2e
mobile-e2e-android-sharded:
name: Maestro Runner Android e2e (shard ${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 75
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
CI: "1"
EXPO_NO_INTERACTIVE: "1"
EXPO_PUBLIC_GOOGLE_PLACES_API_KEY: ${{ secrets.EXPO_PUBLIC_GOOGLE_PLACES_API_KEY }}
E2E_SHARD: ${{ matrix.shard }}
E2E_ARTIFACT_ROOT: .bookmarks/e2e-artifacts/shard-${{ matrix.shard }}

steps:
- name: Checkout
Expand Down Expand Up @@ -92,10 +100,24 @@ jobs:
- name: Install dependencies
run: npm ci

- name: Install Maestro CLI
- name: Install maestro-runner
run: |
curl -Ls "https://get.maestro.mobile.dev" | bash
echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
curl -fsSL "https://open.devicelab.dev/install/maestro-runner" | bash

if command -v maestro-runner >/dev/null 2>&1; then
echo "$(dirname $(command -v maestro-runner))" >> "$GITHUB_PATH"
elif [ -x "$HOME/.maestro-runner/bin/maestro-runner" ]; then
export PATH="$HOME/.maestro-runner/bin:$PATH"
echo "$HOME/.maestro-runner/bin" >> "$GITHUB_PATH"
elif [ -x "$HOME/.local/bin/maestro-runner" ]; then
export PATH="$HOME/.local/bin:$PATH"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "maestro-runner not found after installation!"
exit 1
fi

maestro-runner --version

- name: Write mobile e2e env
run: |
Expand All @@ -110,79 +132,22 @@ jobs:
JAVA_HOME=${JAVA_HOME}
EOF

- name: Run Maestro e2e suite on Android emulator
- name: Run maestro-runner shard on Android emulator
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
arch: x86_64
target: google_apis
profile: pixel_7
profile: pixel_5
disable-animations: true
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none
script: |
set -euo pipefail
mkdir -p .bookmarks

cleanup() {
if [ -n "${METRO_PID:-}" ]; then
kill "${METRO_PID}" >/dev/null 2>&1 || true
fi
if [ -n "${BACKEND_PID:-}" ]; then
kill "${BACKEND_PID}" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT

adb reverse tcp:8081 tcp:8081
adb reverse tcp:8787 tcp:8787

npm run mobile:e2e:backend:start > .bookmarks/backend-e2e.log 2>&1 &
BACKEND_PID=$!

for attempt in $(seq 1 60); do
if curl --fail --silent http://127.0.0.1:8787/health > /dev/null; then
break
fi

if [ "$attempt" -eq 60 ]; then
echo "Backend failed to become healthy."
exit 1
fi

sleep 2
done

npm --workspace @bookmarks/mobile run start -- --dev-client --port 8081 --localhost --non-interactive > .bookmarks/metro.log 2>&1 &
METRO_PID=$!

for attempt in $(seq 1 90); do
if curl --silent http://127.0.0.1:8081/status | grep -q "packager-status:running"; then
break
fi

if [ "$attempt" -eq 90 ]; then
echo "Metro failed to start."
exit 1
fi

sleep 2
done

pushd apps/mobile/android > /dev/null
./gradlew assembleDebug --no-daemon
popd > /dev/null

adb install -r apps/mobile/android/app/build/outputs/apk/debug/app-debug.apk

npm run mobile:e2e:android
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -memory 6144 -no-snapshot-load -no-snapshot-save
script: bash scripts/mobileE2eAndroidShard.sh

- name: Upload e2e artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: mobile-e2e-android-artifacts
name: mobile-e2e-android-artifacts-shard-${{ matrix.shard }}
if-no-files-found: warn
path: |
.bookmarks/e2e-artifacts
.bookmarks/backend-e2e.log
.bookmarks/metro.log
.bookmarks/e2e-artifacts/shard-${{ matrix.shard }}
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ npm test # Run Node integration + runtime smoke tests

See `building.md` for full setup instructions.

## Physical Device Development (Android)

When running against a **physical Android device** (not an emulator), the standard `10.0.2.2` loopback alias does not work. Use ADB reverse proxy so the device can reach the local backend:

```bash
# 1. Connect device via wireless debugging (Developer options > Wireless debugging)
adb connect <device-ip>:<port>

# 2. Set up reverse proxy — forwards device's localhost:8787 → dev machine's localhost:8787
adb reverse tcp:8787 tcp:8787

# 3. Start the backend
npm run backend:start

# 4. In apps/mobile/.env, switch the backend URL to local:
# EXPO_PUBLIC_BOOKMARKS_BACKEND_URL=http://127.0.0.1:8787

# 5. Start Metro
npm start
```

Re-run `adb reverse tcp:8787 tcp:8787` any time the device reconnects. The reverse tunnel does not persist across ADB daemon restarts.

## Testing

Integration tests exist for schema migrations, CLI/backend runtime smoke checks, and sign-out wipe behavior.
Expand Down
42 changes: 42 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Dockerfile for bookmarks backend deployment on Fly.io
# Uses Node 22 for built-in node:sqlite support.

FROM node:22-slim

WORKDIR /app

# Copy package manifests first for layer caching
COPY package.json package-lock.json* ./
COPY apps/backend/package.json apps/backend/package.json
COPY apps/mobile/package.json apps/mobile/package.json

# Install workspace dependencies (production only, skip optional/mobile-heavy deps)
RUN npm install --omit=dev --ignore-scripts 2>/dev/null; \
npm install tsx --save-dev 2>/dev/null; \
true

# Copy source modules needed by the backend
COPY apps/backend/ apps/backend/
COPY core/ core/
COPY schema/ schema/
COPY db/ db/
COPY auth/ auth/
COPY http/ http/
COPY sync/ sync/
COPY share/ share/
COPY places/ places/
COPY collections/ collections/
COPY preferences/ preferences/
COPY tsconfig.json tsconfig.base.json ./

# Create data directory for SQLite
RUN mkdir -p /data

ENV NODE_ENV=production
ENV BOOKMARKS_BACKEND_HOST=0.0.0.0
ENV BOOKMARKS_BACKEND_PORT=8080
ENV BOOKMARKS_BACKEND_DB_PATH=/data/backend.sqlite

EXPOSE 8080

CMD ["npx", "tsx", "apps/backend/src/index.ts"]
43 changes: 19 additions & 24 deletions apps/backend/src/e2eStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,26 @@ interface SeedUser {
preferenceTheme: string;
}

const createSeedUser = (suffix: string, latitude: number, longitude: number, preferenceTheme: string): SeedUser => {
const uppercaseSuffix = suffix.toUpperCase();

return {
userId: `e2e-user-${suffix}`,
email: `e2e-user-${suffix}@bookmarks.test`,
name: `E2E User ${uppercaseSuffix}`,
seededPlaceId: `e2e-place-${suffix}-1`,
seededCollectionId: `e2e-collection-${suffix}-1`,
latitude,
longitude,
placeName: `Seeded Place ${uppercaseSuffix}`,
collectionName: `Seeded Collection ${uppercaseSuffix}`,
preferenceTheme,
};
};

const seededUsers: SeedUser[] = [
{
userId: 'e2e-user-a',
email: 'e2e-user-a@bookmarks.test',
name: 'E2E User A',
seededPlaceId: 'e2e-place-a-1',
seededCollectionId: 'e2e-collection-a-1',
latitude: 53.3498,
longitude: -6.2603,
placeName: 'Seeded Place A',
collectionName: 'Seeded Collection A',
preferenceTheme: 'basalt',
},
{
userId: 'e2e-user-b',
email: 'e2e-user-b@bookmarks.test',
name: 'E2E User B',
seededPlaceId: 'e2e-place-b-1',
seededCollectionId: 'e2e-collection-b-1',
latitude: 40.7128,
longitude: -74.006,
placeName: 'Seeded Place B',
collectionName: 'Seeded Collection B',
preferenceTheme: 'stone',
},
createSeedUser('a', 53.3498, -6.2603, 'basalt'),
createSeedUser('b', 40.7128, -74.006, 'stone'),
];

const seedDatabase = async (databasePath: string): Promise<void> => {
Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# Copy this file to apps/mobile/.env and replace placeholders with real values.
# Mobile env template.
# Preferred setup in this repo is to keep values in root .env (single source)
# and symlink apps/mobile/.env -> ../../.env.

# Required for Google Places search and Android Maps key injection.
EXPO_PUBLIC_GOOGLE_PLACES_API_KEY=your_google_places_api_key
Expand All @@ -7,6 +9,13 @@ JAVA_HOME=/usr/lib/jvm/java-17-openjdk
# Optional: local backend used by mobile auth/sync in development.
EXPO_PUBLIC_BOOKMARKS_BACKEND_URL=http://127.0.0.1:8787

# Optional: Google OAuth client IDs (required for Google sign in).
# Create these in Google Cloud Console OAuth credentials.
EXPO_PUBLIC_GOOGLE_EXPO_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=

# Optional: dev-only test auth controls.
EXPO_PUBLIC_BOOKMARKS_DEV_TEST_AUTH=1
EXPO_PUBLIC_BOOKMARKS_DEV_TEST_USER=dev-user
Expand Down
16 changes: 13 additions & 3 deletions apps/mobile/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import React, { useEffect, useState } from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { View, Text, ActivityIndicator, StyleSheet, Platform } from 'react-native';
import { View, Text, ActivityIndicator, StyleSheet, Platform, LogBox } from 'react-native';
import * as Linking from 'expo-linking';

import AppNavigator from './navigation/AppNavigator';
Expand All @@ -15,6 +15,12 @@ import { colors } from './styles/colors';
import { AuthProvider } from './context/AuthContext';
import { isE2eModeEnabled } from './config/e2e';

if (isE2eModeEnabled) {
LogBox.ignoreAllLogs(true);
}

const isDiagnosticsEnabled = isE2eModeEnabled || __DEV__;

// Deep linking configuration
const prefix = Linking.createURL('/');

Expand All @@ -26,8 +32,12 @@ const getWebOrigin = () => {
return '';
};

const linkingPrefixes = [prefix, getWebOrigin(), 'https://bookmarks.app', 'bookmarks://'].filter(
(value) => typeof value === 'string' && value.length > 0
);

const linking = {
prefixes: [prefix, getWebOrigin(), 'https://bookmarks.app', 'bookmarks://'],
prefixes: linkingPrefixes,
config: {
screens: {
SharedCollection: {
Expand All @@ -36,7 +46,7 @@ const linking = {
data: (data) => data,
},
},
...(isE2eModeEnabled ? { Diagnostics: 'diagnostics' } : {}),
...(isDiagnosticsEnabled ? { Diagnostics: 'diagnostics' } : {}),
Main: {
screens: {
LibraryTab: {
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="bookmarks"/>
</intent-filter>
</activity>
</application>
</manifest>
4 changes: 3 additions & 1 deletion apps/mobile/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "bookmarks",
"userInterfaceStyle": "dark",
"newArchEnabled": true,
"splash": {
Expand Down Expand Up @@ -40,7 +41,8 @@
"expo-web-browser",
"expo-apple-authentication",
"expo-sqlite",
"expo-secure-store"
"expo-secure-store",
"@react-native-google-signin/google-signin"
],
"extra": {
"eas": {
Expand Down
Loading