Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
17b78df
feat(native): initialize Expo bare-workflow workspace for RN migration
claude May 19, 2026
6644361
feat(native): add remaining screens and live-session components
claude May 19, 2026
2e6d191
feat(native): merge native-build — Expo RN workspace for mobile migra…
claude May 19, 2026
b0dd16b
feat(native): complete RN screen ports and fix TypeScript build
claude May 20, 2026
7bcc144
ci: add EAS Build workflow for Android APK
claude May 20, 2026
147992f
ci: switch Android build to local Gradle (no EAS/Expo account needed)
claude May 20, 2026
e3a477b
ci: bump all actions to v5 to fix Node.js 20 deprecation warning
claude May 20, 2026
4e68250
ci: pin cache/upload-artifact to v4, keep checkout/setup-* on v5
claude May 20, 2026
4ced993
fix: Android CI parse error, BLE scan/broadcast mismatch, review nits
claude May 20, 2026
d996b17
fix(native): add .npmrc with legacy-peer-deps=true to unblock npm ci
claude May 20, 2026
1d4c1d2
ci(android): add diagnostic step + verbose npm ci to surface 30s failure
claude May 20, 2026
226f185
chore(native): use `expo run:android/ios` scripts for bare workflow
claude May 20, 2026
ee49899
fix(native): add missing clsx + tailwind-merge deps used by lib/utils.ts
claude May 20, 2026
8db8dc8
fix(native): pin react-native-reanimated to ~4.1.1 (Expo 54 compatible)
claude May 20, 2026
90e3a05
fix(native): downgrade react-native-reanimated to v3.16 (avoid new-ar…
claude May 20, 2026
2d30a8a
fix(native): pin all deps to Expo SDK 54 compatible versions
claude May 20, 2026
e5c1823
feat(native): enable expo web preview for screen visual checks
claude May 20, 2026
0b083dc
fix(native): remove react-native-worklets (autolinked native module)
claude May 20, 2026
3d81568
docs: rewrite README with full attendance history narrative and embed…
King-Austin May 27, 2026
8d27227
chore: resolve merge conflict — keep full README rewrite, update biom…
King-Austin May 27, 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
93 changes: 92 additions & 1 deletion .github/workflows/build-android.yml
Original file line number Diff line number Diff line change
@@ -1 +1,92 @@
name: Build Android

on:
 push:
 branches: [ main, master ]
 pull_request:
 branches: [ main, master ]

env:
 NODE_VERSION: '20'

jobs:
 build:
 runs-on: ubuntu-latest
 steps:
 - name: Checkout code
 uses: actions/checkout@v4

 - name: Setup Node.js
 uses: actions/setup-node@v4
 with:
 node-version: ${{ env.NODE_VERSION }}
 cache: 'npm'

 - name: Setup Java
 uses: actions/setup-java@v4
 with:
 distribution: 'temurin'
 java-version: '17'

 - name: Install dependencies
 run: npm ci

 - name: Build web assets
 run: npm run build

 - name: Setup Capacitor
 run: npx cap sync android

 - name: Build Android APK (debug)
 run: |
 cd android
 ./gradlew assembleDebug

 - name: Build Android AAB (release unsigned)
 run: |
 cd android
 ./gradlew bundleRelease

 - name: Upload APK
 uses: actions/upload-artifact@v4
 with:
 name: android-debug-apk
 path: android/app/build/outputs/apk/debug/app-debug.apk

 - name: Upload AAB
 uses: actions/upload-artifact@v4
 with:
 name: android-release-aab
 path: android/app/build/outputs/bundle/release/app-release.aab

name: Build Android APK

on:
push:
branches: [main]
paths:
- 'native/**'
- '.github/workflows/build-android.yml'
pull_request:
branches: [main]
paths:
- 'native/**'
- '.github/workflows/build-android.yml'
workflow_dispatch:

concurrency:
group: android-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
name: Gradle Build — Android debug APK
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'
cache-dependency-path: native/package-lock.json

- name: Setup Java 17
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'

- name: Install native deps
working-directory: native
run: npm ci

- name: TypeScript check
working-directory: native
run: npx tsc --noEmit

- name: Expo Prebuild (generate android/ folder)
working-directory: native
env:
EXPO_PUBLIC_SUPABASE_URL: ${{ secrets.EXPO_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co' }}
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${{ secrets.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY || 'placeholder_key' }}
EXPO_PUBLIC_BIOMETRIC_SERVER_URL: ${{ secrets.EXPO_PUBLIC_BIOMETRIC_SERVER_URL || 'https://placeholder.example.com' }}
run: npx expo prebuild --platform android --clean --no-install

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('native/android/**/*.gradle*', 'native/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-

- name: Build debug APK
working-directory: native/android
run: ./gradlew assembleDebug --no-daemon --stacktrace

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: smart-campus-debug-${{ github.sha }}
path: native/android/app/build/outputs/apk/debug/app-debug.apk
retention-days: 14

- name: Job summary
if: success()
run: |
APK_PATH="native/android/app/build/outputs/apk/debug/app-debug.apk"
if [ -f "$APK_PATH" ]; then
APK_SIZE=$(du -sh "$APK_PATH" | cut -f1)
echo "## Android Debug APK" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| | |" >> "$GITHUB_STEP_SUMMARY"
echo "|---|---|" >> "$GITHUB_STEP_SUMMARY"
echo "| Size | $APK_SIZE |" >> "$GITHUB_STEP_SUMMARY"
echo "| Commit | \`${{ github.sha }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Download the APK from the **Artifacts** section above." >> "$GITHUB_STEP_SUMMARY"
fi
163 changes: 163 additions & 0 deletions MOBILE_MIGRATION_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Smart Campus Presence — Mobile Migration Plan
## Web (Capacitor) → React Native + Expo (Bare Workflow)

> **Status**: Phase 1 — Approved, implementation in progress on branch `native-build`.
> **Expo workspace**: `/native/` directory in repo root. Web app (`/src`) is untouched.

---

## 1. Route → Screen Mapping

| Web Route | Web Component (lines) | RN Screen File | Navigator | Key Conversion Notes |
|---|---|---|---|---|
| `/` | `Landing.tsx` (110) | `screens/LandingScreen.tsx` | `AuthStack` | No Capacitor calls. Framer Motion glows → Reanimated. CTA buttons navigate to Login |
| `/login` | `Login.tsx` (102) | `screens/LoginScreen.tsx` | `AuthStack` | `<form>` → controlled `TextInput`s. `sonner` toast → `react-native-toast-message`. Supabase auth unchanged |
| `/register/student` | `StudentRegister.tsx` (457) | `screens/student-register/BasicInfoScreen.tsx` `CourseSelectScreen.tsx` `FaceEnrollScreen.tsx` | `RegisterStack` (nested) | Split 3-step wizard into 3 screens. `LivenessScanner` (MediaPipe WebAssembly) → `expo-camera` still photo → POST to `/enroll` on FastAPI |
| `/register/lecturer` | `LecturerRegister.tsx` (128) | `screens/LecturerRegisterScreen.tsx` | `AuthStack` | Single form, straight port |
| `/student` | `StudentDashboard.tsx` (563) | `screens/StudentDashboardScreen.tsx` | `StudentTabs` (Bottom Tabs) | `BottomNav.tsx` dropped; `@react-navigation/bottom-tabs` used instead. Backdrop blur → `expo-blur` BlurView |
| `/student/verify/:sessionId` | `AttendanceVerification.tsx` (457) | `screens/AttendanceVerificationScreen.tsx` | `StudentStack` (modal) | `@capacitor/geolocation` → `expo-location`. BLE central scan → `react-native-ble-manager`. Camera → `expo-camera` → FastAPI `/verify`. `@capacitor/haptics` → `expo-haptics` |
| `/ledger/:sessionId` | `AttendanceLedger.tsx` (210) | `screens/AttendanceLedgerScreen.tsx` | `StudentStack` | `<ul>` list → `FlatList`. Search stays on local state. Supabase query unchanged |
| `/lecturer` | `LecturerDashboard.tsx` (391) | `screens/LecturerDashboardScreen.tsx` | `LecturerTabs` (Bottom Tabs) | `recharts` EngagementChart → `victory-native`. Supabase Realtime unchanged |
| `/lecturer/create-session` | `CreateSession.tsx` (239) | `screens/CreateSessionScreen.tsx` | `LecturerStack` | BLE peripheral broadcast → `react-native-ble-advertiser` (Android) / `react-native-ble-plx` (iOS). `expo-location` for GPS anchor |
| `/lecturer/session/:sessionId` | `LiveSession.tsx` (205) | `screens/LiveSessionScreen.tsx` | `LecturerStack` | Realtime attendance feed via Supabase unchanged. `recharts` → `victory-native` |
| `*` | `NotFound.tsx` (24) | `screens/NotFoundScreen.tsx` | Root fallback | Trivial port |

---

## 2. Navigation Hierarchy

```
RootNavigator
├── (if unauthenticated) → AuthStack
│ ├── LandingScreen
│ ├── LoginScreen
│ ├── RegisterStack
│ │ ├── BasicInfoScreen
│ │ ├── CourseSelectScreen
│ │ └── FaceEnrollScreen
│ └── LecturerRegisterScreen
│
├── (if role === 'student') → StudentTabs (Bottom Tabs)
│ ├── Tab: Home → StudentDashboardScreen
│ ├── Stack: AttendanceVerificationScreen (pushed from session card)
│ └── Stack: AttendanceLedgerScreen
│
└── (if role === 'lecturer') → LecturerTabs (Bottom Tabs)
├── Tab: Home → LecturerDashboardScreen
├── Stack: CreateSessionScreen
└── Stack: LiveSessionScreen
```

Auth gate: `RootNavigator` calls `useProfile()` on mount. If no session → `AuthStack`. If `role === 'student'` → `StudentTabs`. If `role === 'lecturer'` → `LecturerTabs`.

---

## 3. Native Plugin Replacement Map

| Capacitor Plugin | Expo / RN Replacement | Notes |
|---|---|---|
| `@capacitor/camera` | `expo-camera` | `Camera.takePictureAsync()` returns base64. Same payload to FastAPI |
| `@capacitor/geolocation` | `expo-location` | `Location.getCurrentPositionAsync()`. Same accuracy settings |
| `@capacitor-community/bluetooth-le` (peripheral) | `react-native-ble-advertiser` | Android peripheral advertising. iOS: limited — show warning to lecturers |
| `@capacitor-community/bluetooth-le` (central/scan) | `react-native-ble-manager` | Student-side scanning for session tokens |
| `@capacitor/haptics` | `expo-haptics` | `Haptics.impactAsync(ImpactFeedbackStyle.Heavy)` — identical feel |
| `@capacitor/device` | `expo-device` + `expo-secure-store` | Device ID via `SecureStore` for binding |
| `@capacitor/status-bar` | `expo-status-bar` | `<StatusBar style="light" />` |
| `@capacitor/app` | React Navigation back handler | `useNavigation().goBack()` + `BackHandler` |

---

## 4. Package Replacement Map

| Web Package | Action | RN Replacement |
|---|---|---|
| `react-router-dom` | Replace | `@react-navigation/native` + `@react-navigation/native-stack` + `@react-navigation/bottom-tabs` |
| `framer-motion` | Replace | `react-native-reanimated` v3 + `react-native-gesture-handler` |
| `@radix-ui/*` (all) | Replace | Custom RN components in `native/src/components/ui/` |
| All `shadcn/ui` (61 files) | Replace | Subset of custom RN components (Button, Card, Input, Badge, Sheet, Tabs) |
| `recharts` | Replace | `victory-native` |
| `sonner` | Replace | `react-native-toast-message` |
| `next-themes` | Replace | Custom `ThemeContext` with `AsyncStorage` persistence |
| `embla-carousel-react` | Replace | `FlatList` horizontal |
| `vaul` (drawer) | Replace | `@gorhom/bottom-sheet` |
| `@mediapipe/tasks-vision` | **Drop** — backend only | FastAPI `/verify` handles all face recognition |
| `@supabase/supabase-js` | Keep | Auth storage: `AsyncStorage` instead of `localStorage` |
| `@tanstack/react-query` | Keep | No changes |
| `react-hook-form` | Keep | No changes |
| `zod` | Keep | No changes |
| `lucide-react` | Replace | `lucide-react-native` (same component names) |
| `tailwindcss` + `className` | Keep via NativeWind | NativeWind v4 babel plugin compiles `className` for RN |

---

## 5. Files Copied Verbatim (zero or minimal changes)

| Source | Destination | Change |
|---|---|---|
| `src/integrations/supabase/types.ts` | `native/src/integrations/supabase/types.ts` | None |
| `src/hooks/useProfile.ts` | `native/src/hooks/useProfile.ts` | None |
| `src/hooks/useLiveSessions.ts` | `native/src/hooks/useLiveSessions.ts` | None |
| `src/hooks/useSessionData.ts` | `native/src/hooks/useSessionData.ts` | None |
| `src/hooks/useAttendanceStats.ts` | `native/src/hooks/useAttendanceStats.ts` | None |
| `src/hooks/useLecturerData.ts` | `native/src/hooks/useLecturerData.ts` | None |
| `src/hooks/useBiometrics.ts` | `native/src/hooks/useBiometrics.ts` | `import.meta.env.VITE_*` → `process.env.EXPO_PUBLIC_*`. `sonner` → `react-native-toast-message` |
| `src/lib/utils.ts` | `native/src/lib/utils.ts` | None (`clsx` + `tailwind-merge` work in RN) |
| `src/constants/*` | `native/src/constants/*` | None |
| `src/types/*` | `native/src/types/*` | None |

---

## 6. Files Fully Rewritten for RN

| File | Reason |
|---|---|
| `native/App.tsx` | `BrowserRouter` → `NavigationContainer`. Web providers stripped |
| `native/src/integrations/supabase/client.ts` | `localStorage` → `AsyncStorage`. `VITE_` → `EXPO_PUBLIC_` |
| `native/src/hooks/useBlePeripheral.ts` | Capacitor BLE → `react-native-ble-advertiser` peripheral |
| `native/src/hooks/useBleScanner.ts` | NEW: student-side central scan via `react-native-ble-manager` |
| `native/src/lib/ble.ts` | Capacitor → `react-native-ble-advertiser` |
| `native/src/lib/geo.ts` | Capacitor → `expo-location` |
| `native/src/lib/device.ts` | Capacitor → `expo-device` + `expo-secure-store` |
| All 11 screen files | Web HTML → RN core components |
| `native/src/components/verification/CameraCapture.tsx` | Replaces `LivenessScanner.tsx` — no MediaPipe, just `expo-camera` capture |

---

## 7. Styling Rules Applied in Conversion

| Web Pattern | RN Equivalent |
|---|---|
| `<div className="...">` | `<View className="...">` (NativeWind) |
| `<p className="...">` / `<span>` / `<h1>` | `<Text className="...">` |
| `<img src={x} alt={y}>` | `<Image source={x} accessibilityLabel={y} />` |
| `<button onClick={fn}>` | `<Pressable onPress={fn}>` |
| `<input onChange={fn}>` | `<TextInput onChangeText={fn}>` |
| `className="backdrop-blur-lg bg-card/95"` | `<BlurView intensity={50} tint="dark" className="bg-card/95">` |
| `<motion.div initial animate>` | `<Animated.View style={animatedStyle}>` (Reanimated) |
| `lucide-react` icon | `lucide-react-native` (same import name) |
| `min-h-screen` | `flex: 1` |
| `fixed bottom-0` | `position: 'absolute', bottom: 0` or `@react-navigation/bottom-tabs` |
| `safe-top` CSS class | `<SafeAreaView>` from `react-native-safe-area-context` |

---

## 8. EAS Build Profiles

| Profile | Purpose | Output |
|---|---|---|
| `development` | Local dev client (supports native modules) | Dev client app |
| `preview` | Internal distribution / testing APK | `.apk` (Android) |
| `production` | Store submission | `.aab` (Android), `.ipa` (iOS) |

---

## 9. Approval Gate (per original brief)

- [x] Phase 1: `MOBILE_MIGRATION_PLAN.md` written — **awaiting validation**
- [ ] Phase 2: Expo workspace initialized (`npx create-expo-app native`)
- [ ] Phase 3: Core packages installed
- [ ] Phase 4: Config files written (babel, metro, tailwind, app.json, eas.json)
- [ ] Phase 5: Infrastructure ported (navigation, Supabase client, hooks, lib/)
- [ ] Phase 6: All screens converted (3 parallel agent groups)
- [ ] Phase 7: EAS Build configured
- [ ] Phase 8: Pre-flight checks pass (`tsc`, `eslint`, `expo doctor`)
Loading
Loading