Files
FarmaFinder/NATIVE.md
Antoni Nuñez Romeu 17500e0567
Build & Push Docker Images / test-backend (push) Successful in 21s
Build & Push Docker Images / test-frontend (push) Successful in 22s
Build & Push Docker Images / build-backend (push) Successful in 16s
Build & Push Docker Images / build-frontend (push) Successful in 17s
iOS Build using GH Actions
2026-06-29 13:02:16 +02:00

12 KiB
Raw Permalink Blame History

Native Mobile App Build Guide

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


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:

./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:

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

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

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

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