Files
halobestie-clone/client_app/.maestro
Ramadhan Sjamsani 3a0cdf5c4e Phase 5/6 polish: end-session flow, notif sound on API 33+, Xendit webview
Customer end-of-session (figma §6):
- PricingBottomSheet: ghost "cukup, akhiri sesi" CTA + dedup divider
- chat_screen._runEndSessionFlow chains ConfirmEndStep1 → ConfirmEndStep2
  → ClosingMessageSheet (or "lewati saja" → close + /home). The four
  popup/sheet widgets already existed; this commit just wires them
- showModalBottomSheet: showDragHandle=false to suppress the Material 3
  auto-injected handle that was stacking with our own pill

Notification sound on API 33+:
- Bump channel halobestie_chat_v1 → halobestie_chat_v2, created from
  native Kotlin in MainActivity.kt with AudioAttributes contentType
  CONTENT_TYPE_SONIFICATION. flutter_local_notifications' default of
  CONTENT_TYPE_UNKNOWN was causing Android 13 to silently drop audio
  focus while the notification still posted (isNoisy=true). Both apps
- Backend FCM payload channelId updated to v2
- AndroidManifest meta-data: default_notification_icon + color → brand
  silhouette tinted pink instead of generic Android bell. Both apps

Customer pairing reliability:
- pairing_notifier: applyPairedFromPush({sessionId, mitraName}) unsticks
  searching screen when WS push failed and FCM/active-session-poll is
  the first signal. Idempotent across PairingSearchingData,
  PairingTargetedWaitingData, PairingErrorData (covers ALREADY_ACTIVE)
- notification_service: dispatches every FCM data payload to an
  onDataMessage callback (foreground + tap + cold-start). main.dart
  wires that to applyPairedFromPush on type=='paired'. Foreground
  'paired' no longer renders a local banner — screen self-advances
- main.dart activeSession listener also calls applyPairedFromPush when
  a session appears server-side while pairing is in a waiting state.
  Covers stale ALREADY_ACTIVE recovery without a full page refresh

Auth refresh token race:
- auth_notifier._refreshFromStorage shares a single in-flight Future
  across all callers (Auth.build + 401-retry path). Backend rotates
  refresh tokens, so concurrent callers using the same stored token
  would race → loser 401s → catch wipes flutter_secure_storage → user
  appears logged out after kill+reopen

Polish:
- method_pick_screen: resizeToAvoidBottomInset=false — prevents the
  one-frame overflow when entering with the previous screen's keyboard
  still animating out
- bestie_history: BestieHistoryItem now carries `status` (backend
  already returns it). Removed _rawHistoryProvider that fetched the
  same endpoint just to read status; the two providers could go out
  of sync mid-rebuild and throw RangeError(length) on indexing

Xendit Stage 8 (carried from WIP):
- xendit_checkout_screen: embedded webview hosting Xendit's invoice
  page (intercepts halobestie:// deeplink + return-page URLs for
  deterministic pop)
- waiting_payment_screen: auto-pushes the webview when the backend
  payload includes xendit_invoice_url; spinner card + "Buka ulang
  halaman pembayaran" CTA for the QR-fallback path
- pubspec: webview_flutter ^4.13.0

Maestro infra:
- subflows/onboarding_returning_user: drop the "Mulai" carousel wait
  (splash auto-advances since 2026-05-26); tap phone-field hint
  instead of point; drop hideKeyboard (sends BACK → /home when the
  IME isn't actually up)
- New flow ts-customer-06-01-end_session_via_timeup_sheet: drives
  the full path to the chat-expired banner. Last step blocked by a
  Maestro+Flutter gesture quirk on the perpanjang ElevatedButton
  (raw `adb input tap` works at the same coords). Documented in
  memory; deeplink fixture or manual verify recommended
- ChatExpiredBanner button wrapped with Semantics(identifier:
  'chat_extend_button', button: true, onTap: …) — good hygiene for
  future tests even though it doesn't fix the dadb tap issue

.dev/: tracked wsl_emulator_bridge.ps1 + wsl_tcp_relay.py for
Maestro-on-WSL setup (Windows-side netsh portproxy + WSL-side
loopback relays). Both referenced from existing CLAUDE.md notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 21:45:46 +08:00
..

client_app Maestro flows

End-to-end UI automation for the customer Flutter app using Maestro. Single-emulator + curl-as-mitra pattern — the customer app is driven by real Maestro touches; the mitra side is simulated via backend API calls fired from runScript steps.

One-time install

Maestro is a global CLI (not a project dependency). Install on your dev machine once:

curl -Ls "https://get.maestro.mobile.dev" | bash

Verify with maestro --version. See the Maestro install docs for Homebrew / chocolatey / Docker alternatives.

You also need:

  • adb on your PATH (comes with Android Studio's platform-tools)
  • jq for the helper scripts (apt install jq / brew install jq)
  • One Android emulator OR one connected device — only one at a time (per project decision)

Folder layout

.maestro/
├── README.md                 # this file
├── config.yaml               # shared env: app IDs, backend URL, test credentials
├── flows/                    # the YAML test scripts
│   ├── 01_smoke.yaml
│   ├── 02_cta_disabled_when_no_mitra.yaml
│   └── 03_payment_to_chat_happy.yaml
└── scripts/                  # bash helpers invoked by `runScript` steps
    ├── mitra_accept_latest.sh
    └── force_all_mitras_offline.sh

Configure for your environment

Edit .maestro/config.yaml and fill in:

  • BACKEND_URL — must match the --dart-define=API_BASE_URL=... value the installed APK was built with
  • TEST_MITRA_ID and TEST_MITRA_JWT — used by the curl harness to "accept" requests from the customer's blast

The config file is committed because the values are dev-environment defaults. Sensitive credentials (real JWTs, CC operator tokens) should be passed at runtime instead — see "Per-machine overrides" below.

Run a flow

Single emulator (typical case — Maestro auto-picks the only attached device):

# from repo root or anywhere
maestro test client_app/.maestro/flows/01_smoke.yaml

# or run all flows in the directory
maestro test client_app/.maestro/flows/

If both an emulator and a real device happen to be connected, list them and pick one explicitly:

adb devices                                       # list attached devices
maestro --device emulator-5554 test client_app/.maestro/flows/01_smoke.yaml

Per-machine overrides

Override any config.yaml value at runtime with --env:

maestro test \
  --env BACKEND_URL=http://192.168.99.10:3000 \
  --env TEST_MITRA_JWT=eyJhbGc... \
  client_app/.maestro/flows/03_payment_to_chat_happy.yaml

Or export shell variables — runScript steps inherit them:

export CC_JWT=eyJhbGc...
maestro test client_app/.maestro/flows/02_cta_disabled_when_no_mitra.yaml

Single-emulator + curl pattern

Phase 3.7 flows often need a customer + a mitra acting in concert. Instead of running two emulators (RAM-heavy, flaky), the flows drive the customer side with Maestro and simulate the mitra via backend curl calls:

  1. Maestro flow drives customer up to the "Mencari Bestie..." state
  2. runScript: ../scripts/mitra_accept_latest.sh fires POST /api/mitra/chat-requests/:id/accept against the backend, using a pre-minted mitra JWT
  3. Maestro flow asserts the customer screen transitions to "Bestie Ditemukan" via the WS round-trip

This works for ~90% of multi-actor scenarios — including all the Section D ("Curhat lagi") and Section J ("Mitra goes offline mid-session") tests in phase3.7-testing.md. The 10% that needs both UIs running (e.g., asserting the mitra-side overlay countdown displays correctly) is in mitra_app/.maestro/ and runs separately.

Adding a new flow

Pick a Phase 3.7 testing checklist scenario (see phase3.7-testing.md), then:

  1. Copy an existing flow as a template (e.g., 03_payment_to_chat_happy.yaml)
  2. Update the pre-req comment, the steps, and the assertions
  3. If you need a "second actor" action, add a bash helper under scripts/ and call it via runScript:
  4. If you need new env vars, add them to config.yaml with sensible defaults

Tips

  • Find the right text to tap onmaestro studio opens a live UI inspector showing every visible label/widget. Run it while the app is on the screen you care about.
  • Slow it down for debuggingmaestro test --debug-output ./debug flows/foo.yaml saves screenshots + logs per step.
  • Add flows incrementally — Maestro's reload-on-save in maestro studio makes iteration fast.
  • Don't commit screenshots / debug output — add .maestro/output/ and .maestro/screenshots/ to .gitignore if you generate them locally.

Troubleshooting

  • maestro: device not found → run adb devices; if empty, start an emulator (emulator -avd <name>) or plug in a USB device with debugging enabled.
  • Element not visible errors → use maestro studio to inspect actual labels — they may have changed since the flow was written.
  • Flow hangs at assertVisible waiting for backend → check BACKEND_URL matches the APK's build-time value (grep API_BASE_URL build.gradle).
  • runScript exits non-zero → run the script directly to see its error: bash client_app/.maestro/scripts/mitra_accept_latest.sh. Most often a missing env var or stale JWT.