# Native Mobile App Build Guide FarmaFinder ships as a PWA and is wrapped for the App Store / Play Store via [Capacitor 8](https://capacitorjs.com). The web frontend in `frontend/` is bundled into each native shell — there's no separate codebase for Android or iOS. ``` FarmaFinder/ ├── frontend/ # React + Vite PWA (the actual UI) ├── backend/ # Node API ├── android/ # Capacitor-generated Android Studio project ├── ios/ # Capacitor-generated Xcode project └── capacitor.config.json ``` The `android/` and `ios/` folders are real native projects — open them in their IDEs, change Swift / Kotlin / Gradle / xib as needed. --- ## One-time setup ### Common (any OS) ```bash npm run install:all # root + frontend + backend deps ``` ### Android prerequisites - **JDK 21** (or 17). Install via your package manager or [Adoptium](https://adoptium.net/). - **Android Studio** (Hedgehog or later) with the Android SDK Platform 34 installed. - Set `ANDROID_HOME` and add `platform-tools` to `PATH`, e.g.: ```bash export ANDROID_HOME="$HOME/Android/Sdk" export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH" ``` - Open `android/` once in Android Studio so Gradle resolves the dependencies. ### iOS prerequisites (macOS only) - **Xcode 15+** from the App Store. Run it once and accept the licence. - **Command Line Tools**: `xcode-select --install` - **CocoaPods**: `sudo gem install cocoapods` (or `brew install cocoapods`) - After cloning, run pod install inside `ios/App`: ```bash cd ios/App && pod install && cd - ``` --- ## Day-to-day workflow 1. **Build the web bundle** ```bash npm run build:web ``` Produces `frontend/dist/`, which Capacitor copies into the native projects. 2. **Sync into the native shells** ```bash npm run cap:sync ``` - Copies `frontend/dist/` → `android/app/src/main/assets/public/` and `ios/App/App/public/` - Re-runs `pod install` if new plugins were added - Updates `capacitor.config.json` inside each platform 3. **Open the native project in its IDE** ```bash npm run cap:open:android # Android Studio npm run cap:open:ios # Xcode (macOS only) ``` 4. **Run on a device or emulator** ```bash npm run cap:run:android npm run cap:run:ios ``` These build the web bundle, sync, and launch on the first connected device/emulator. After editing anything under `frontend/`, repeat steps 1 + 2 (or use `cap:sync`). Editing native files (Gradle, Swift, Kotlin, plist) is done directly inside `android/` and `ios/` — those edits survive `cap sync`. --- ## Pointing the app at the backend The native bundle loads `index.html` from inside the app (`capacitor://localhost/` on iOS, `https://localhost/` on Android by default), so **relative `/api/...` paths do not reach the backend**. Pick one of: ### Option A — wrap the deployed PWA (simplest) Add a `server.url` to `capacitor.config.json` so the WebView loads the production site: ```json { "server": { "url": "https://farmacias.hacecalor.net", "cleartext": false, "androidScheme": "https" } } ``` Run `npm run cap:sync` after the change. The native app is now a thin shell around the live PWA — fastest to ship, but useless without a network. ### Option B — bundled web + absolute API URLs (recommended) Keep the web assets bundled (no `server.url`) and rewrite frontend `fetch()` calls to use an absolute backend URL. The path of least resistance: 1. Add a Vite env var to `frontend/.env.production`: ``` VITE_API_BASE=https://farmacias.hacecalor.net ``` 2. Centralise `fetch` calls through a helper (e.g. `frontend/src/utils/api.js`): ```js const BASE = import.meta.env.VITE_API_BASE || ''; export const apiUrl = (path) => `${BASE}${path.startsWith('/') ? path : '/' + path}`; ``` 3. Replace `fetch('/api/...')` with `fetch(apiUrl('/api/...'))` across the components. 4. On the backend, set `CORS_ORIGIN=https://farmacias.hacecalor.net,capacitor://localhost,https://localhost` so the native WebView origin is allowed. This lets the app work offline for cached UI and only hits the network for live data. --- ## Permissions already declared The two manifests are pre-wired for the features the PWA already uses: **`android/app/src/main/AndroidManifest.xml`** - `INTERNET` — network calls - `ACCESS_FINE_LOCATION` / `ACCESS_COARSE_LOCATION` — "Sort by distance" - `POST_NOTIFICATIONS` — Android 13+ push opt-in **`ios/App/App/Info.plist`** - `NSLocationWhenInUseUsageDescription` — shown by the OS when geolocation is requested - `NSLocationAlwaysAndWhenInUseUsageDescription` — same for background-style requests - `UIBackgroundModes: remote-notification` — required for APNs push delivery If you add features that need new permissions (camera, contacts, etc.), edit those files directly. --- ## App icons & splash screens Capacitor generated placeholder assets: - Android: `android/app/src/main/res/mipmap-*/ic_launcher*.png` - iOS: `ios/App/App/Assets.xcassets/AppIcon.appiconset/` - Splash: `ios/App/App/Assets.xcassets/Splash.imageset/` and the Android theme Replace these with real artwork before shipping. The easiest way is the official asset generator — drop a single 1024×1024 PNG (and a foreground/background for adaptive icons) into a `resources/` folder at the project root and run: ```bash npx @capacitor/assets generate --iconBackgroundColor '#0f766e' --splashBackgroundColor '#0f766e' ``` (install once: `npm install --save-dev @capacitor/assets`) Splash colour and behaviour are configured in `capacitor.config.json` (`plugins.SplashScreen`). The runtime hook in `frontend/src/utils/native.js` hides the splash after React mounts. --- ## Push notifications on native The web PWA uses VAPID + the browser PushManager. **Native iOS and Android cannot use Web Push.** To deliver push from the existing backend to native shells: - **Android** — set up Firebase Cloud Messaging, add `android/app/google-services.json`, install `@capacitor/push-notifications`. The backend then sends to FCM tokens instead of (or in addition to) the existing Web Push subscriptions. - **iOS** — create an APNs key in the Apple Developer portal, enable the Push capability in Xcode, configure the `aps-environment` entitlement, install `@capacitor/push-notifications`. Until those are wired up, the bell button in the native app is a no-op — `pushSupported()` in `frontend/src/utils/notifications.js` returns false because there's no service worker in the native WebView. --- ## Releasing ### Android 1. In Android Studio: **Build → Generate Signed Bundle / APK → Android App Bundle**. 2. Create or reuse a keystore; the password unlocks the release signing config. 3. Bump `versionCode` and `versionName` in `android/app/build.gradle` for each release. 4. Upload the produced `.aab` to Google Play Console. ### iOS 1. In Xcode: select an **Any iOS Device (arm64)** destination → **Product → Archive**. 2. From the Organizer, **Distribute App → App Store Connect**. 3. Bump `CURRENT_PROJECT_VERSION` (build) and `MARKETING_VERSION` (display) under the App target's *General* tab. 4. Submit via App Store Connect. Both stores require: privacy policy URL, screenshots (multiple device sizes), an app description, and an age rating questionnaire. The pharmacy domain typically triggers an extra medical-content review on iOS — keep marketing copy factual. --- ## CI/CD: GitHub Actions (iOS) The project includes a GitHub Actions workflow for automated iOS builds, replacing the previous Bitrise pipeline. ### Workflow file `.github/workflows/ios-build.yml` — builds the iOS app on every `ios-v*` tag push, or manually via `workflow_dispatch`. ### Trigger types | Trigger | Behaviour | |---------|-----------| | `git push ios-v*` tag | Builds, archives, exports IPA, uploads to TestFlight | | `workflow_dispatch` (manual) | Builds and optionally uploads to TestFlight (configurable) | ### Required GitHub Secrets Set these in **Settings → Secrets and variables → Actions**: | Secret | Description | |--------|-------------| | `IOS_P12_BASE64` | Base64-encoded `.p12` distribution certificate | | `IOS_P12_PASSWORD` | Password for the `.p12` file | | `IOS_PROVISION_PROFILE_BASE64` | Base64-encoded `.mobileprovision` file | | `IOS_PROVISION_PROFILE_UUID` | UUID of the provisioning profile (filename in `~/Library/MobileDevice/Provisioning Profiles/`) | | `IOS_PROVISION_PROFILE_NAME` | Name of the provisioning profile (used in Xcode export) | | `IOS_TEAM_ID` | Apple Developer Team ID (10-char alphanumeric) | | `APP_STORE_API_KEY_ID` | App Store Connect API key ID | | `APP_STORE_API_ISSUER_ID` | App Store Connect API issuer ID | | `APP_STORE_API_PRIVATE_KEY` | App Store Connect API private key (`.p8` content) | ### Pipeline steps 1. **Test** (ubuntu-latest) — runs backend + frontend test suites 2. **Build iOS** (macos-14 — Apple Silicon runner): - Installs Node 20, builds web bundle, syncs Capacitor - Installs CocoaPods dependencies - Imports signing certificate + provisioning profile from secrets - Sets `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION` from tag - Builds `.xcarchive`, exports `.ipa` - Uploads IPA as GitHub artifact (30-day retention) - Uploads to TestFlight via `xcrun altool` ### Local iOS build script For local builds on macOS, use the provided script: ```bash ./scripts/build-ios.sh ``` This performs the same steps (build web, sync, pod install, archive, export IPA) without the CI signing/TestFlight steps. Override the export-options path with: ```bash EXPORT_OPTIONS=path/to/export-options.plist ./scripts/build-ios.sh ``` ### Export Options `scripts/export-options.plist` configures the IPA export (App Store method, manual signing, team ID, provisioning profile). The workflow replaces placeholder values from secrets at build time. ### Release flow ```bash # 1. Tag a release git tag ios-v1.2.3 git push origin ios-v1.2.3 # 2. GitHub Actions runs automatically: # test → build → archive → IPA artifact → TestFlight upload # 3. Download the IPA from the Actions Artifacts section if needed # 4. Submit via App Store Connect after TestFlight review ``` ### Migrating from Bitrise The `bitrise.yml` is still present but no longer needed for iOS. To remove it: ```bash rm bitrise.yml ``` Key differences from the Bitrise pipeline: | Aspect | Bitrise | GitHub Actions | |--------|---------|----------------| | Runner | Bitrise macOS stack | `macos-14` (M1 Apple Silicon) | | Signing | Bitrise certificate step | `apple-actions/import-codesign-certs` | | TestFlight upload | `deploy-to-bitrise-io` | `xcrun altool` with App Store Connect API | | Trigger | Tag `ios-v*` | Tag `ios-v*` or manual dispatch | | Artifact | Bitrise dashboard | GitHub Actions Artifacts (30-day retention) | | Cache | Bitrise stack caching | `actions/setup-node` cache | --- ## Updating Capacitor / plugins ```bash npm install @capacitor/core@latest @capacitor/cli@latest \ @capacitor/android@latest @capacitor/ios@latest \ @capacitor/app@latest @capacitor/splash-screen@latest @capacitor/status-bar@latest npm run cap:sync ``` After major upgrades follow the migration guide at https://capacitorjs.com/docs/updating. --- ## Troubleshooting - **`cap sync` says `Could not find platform 'android'`** — you removed the folder. Re-create with `npx cap add android`. - **iOS build fails with `Sandbox: rsync ... deny`** — clean build folder (`Cmd-Shift-K`), then `pod install` inside `ios/App`. - **WebView shows a white screen on Android 14** — verify `android:usesCleartextTraffic` isn't blocking your dev backend (HTTPS only by default in `capacitor.config.json`). - **Geolocation never returns** — Android 14 requires both `ACCESS_FINE_LOCATION` *and* a runtime grant via the system dialog; iOS requires the `Info.plist` keys above plus enabling Location on the simulator (`Features → Location`).