Files
FarmaFinder/NATIVE.md
T
Ichitux c3f0f3c35c
Build & Push Docker Images / test-backend (push) Successful in 53s
Build & Push Docker Images / test-frontend (push) Successful in 58s
Build & Push Docker Images / build-backend (push) Successful in 1m4s
Build & Push Docker Images / build-frontend (push) Failing after 56s
Add iOS & Android build + frontend fixes for desktop
2026-05-26 19:12:31 +02:00

8.4 KiB
Raw Blame History

Native Mobile App Build Guide

FarmaFinder ships as a PWA and is wrapped for the App Store / Play Store via Capacitor 6. 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)

npm run install:all          # root + frontend + backend deps

Android prerequisites

  • JDK 21 (or 17). Install via your package manager or Adoptium.
  • Android Studio (Hedgehog or later) with the Android SDK Platform 34 installed.
  • Set ANDROID_HOME and add platform-tools to PATH, e.g.:
    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:
    cd ios/App && pod install && cd -
    

Day-to-day workflow

  1. Build the web bundle

    npm run build:web
    

    Produces frontend/dist/, which Capacitor copies into the native projects.

  2. Sync into the native shells

    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

    npm run cap:open:android   # Android Studio
    npm run cap:open:ios       # Xcode (macOS only)
    
  4. Run on a device or emulator

    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:

{
  "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.

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):
    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:

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.


Updating Capacitor / plugins

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).