8.4 KiB
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_HOMEand addplatform-toolstoPATH, 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(orbrew install cocoapods) - After cloning, run pod install inside
ios/App:cd ios/App && pod install && cd -
Day-to-day workflow
-
Build the web bundle
npm run build:webProduces
frontend/dist/, which Capacitor copies into the native projects. -
Sync into the native shells
npm run cap:sync- Copies
frontend/dist/→android/app/src/main/assets/public/andios/App/App/public/ - Re-runs
pod installif new plugins were added - Updates
capacitor.config.jsoninside each platform
- Copies
-
Open the native project in its IDE
npm run cap:open:android # Android Studio npm run cap:open:ios # Xcode (macOS only) -
Run on a device or emulator
npm run cap:run:android npm run cap:run:iosThese 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.
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:
- Add a Vite env var to
frontend/.env.production:VITE_API_BASE=https://farmacias.hacecalor.net - Centralise
fetchcalls 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}`; - Replace
fetch('/api/...')withfetch(apiUrl('/api/...'))across the components. - On the backend, set
CORS_ORIGIN=https://farmacias.hacecalor.net,capacitor://localhost,https://localhostso 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 callsACCESS_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 requestedNSLocationAlwaysAndWhenInUseUsageDescription— same for background-style requestsUIBackgroundModes: 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-environmententitlement, 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
- In Android Studio: Build → Generate Signed Bundle / APK → Android App Bundle.
- Create or reuse a keystore; the password unlocks the release signing config.
- Bump
versionCodeandversionNameinandroid/app/build.gradlefor each release. - Upload the produced
.aabto Google Play Console.
iOS
- In Xcode: select an Any iOS Device (arm64) destination → Product → Archive.
- From the Organizer, Distribute App → App Store Connect.
- Bump
CURRENT_PROJECT_VERSION(build) andMARKETING_VERSION(display) under the App target's General tab. - 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 syncsaysCould not find platform 'android'— you removed the folder. Re-create withnpx cap add android.- iOS build fails with
Sandbox: rsync ... deny— clean build folder (Cmd-Shift-K), thenpod installinsideios/App. - WebView shows a white screen on Android 14 — verify
android:usesCleartextTrafficisn't blocking your dev backend (HTTPS only by default incapacitor.config.json). - Geolocation never returns — Android 14 requires both
ACCESS_FINE_LOCATIONand a runtime grant via the system dialog; iOS requires theInfo.plistkeys above plus enabling Location on the simulator (Features → Location).