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

294 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).