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>
This commit is contained in:
134
.dev/wsl_tcp_relay.py
Normal file
134
.dev/wsl_tcp_relay.py
Normal file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""WSL→Windows TCP relay for the Windows-side adb server.
|
||||
|
||||
Two modes:
|
||||
|
||||
static: wsl_tcp_relay.py <listen-port> <target-host> <target-port>
|
||||
Relays 127.0.0.1:<listen-port> -> <target-host>:<target-port>.
|
||||
|
||||
dynamic: wsl_tcp_relay.py --watch-adb <target-host>
|
||||
Polls `adb forward --list` (using the env-configured adb server),
|
||||
and for each host-side TCP port in the listing, ensures a relay
|
||||
127.0.0.1:<port> -> <target-host>:<port> exists. Adds/removes
|
||||
relays as forwards appear/disappear.
|
||||
|
||||
The dynamic mode handles `flutter run` choosing an ephemeral host port (and
|
||||
sometimes rotating it across reconnect attempts) when adb is shared via
|
||||
ADB_SERVER_SOCKET — flutter's adb-forward request lands on the Windows adb
|
||||
server, which binds the listener on 0.0.0.0 of the Windows side. WSL-loopback
|
||||
has nothing on that port, so flutter (connecting to 127.0.0.1:<port> on WSL)
|
||||
fails. This relay fills that gap dynamically.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from typing import Dict, Set
|
||||
|
||||
|
||||
async def _pipe(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
|
||||
try:
|
||||
while data := await reader.read(65536):
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except (ConnectionResetError, BrokenPipeError, OSError):
|
||||
pass
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _handler(target_host: str, target_port: int):
|
||||
async def handle(local_r, local_w):
|
||||
try:
|
||||
remote_r, remote_w = await asyncio.open_connection(target_host, target_port)
|
||||
except Exception as exc:
|
||||
print(f"[relay] connect {target_host}:{target_port} failed: {exc}", file=sys.stderr, flush=True)
|
||||
local_w.close()
|
||||
return
|
||||
await asyncio.gather(_pipe(local_r, remote_w), _pipe(remote_r, local_w))
|
||||
return handle
|
||||
|
||||
|
||||
async def run_static(listen_port: int, target_host: str, target_port: int) -> None:
|
||||
server = await asyncio.start_server(_handler(target_host, target_port), "127.0.0.1", listen_port)
|
||||
print(f"[relay] static 127.0.0.1:{listen_port} -> {target_host}:{target_port}", flush=True)
|
||||
async with server:
|
||||
await server.serve_forever()
|
||||
|
||||
|
||||
async def _adb_forward_ports() -> Set[int]:
|
||||
adb = os.environ.get("ADB", "adb")
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
adb, "forward", "--list",
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
out, _ = await proc.communicate()
|
||||
ports: Set[int] = set()
|
||||
for line in out.decode("utf-8", "replace").splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[1].startswith("tcp:"):
|
||||
try:
|
||||
ports.add(int(parts[1][4:]))
|
||||
except ValueError:
|
||||
pass
|
||||
return ports
|
||||
|
||||
|
||||
async def run_dynamic(target_host: str, poll_seconds: float = 1.0) -> None:
|
||||
active: Dict[int, asyncio.AbstractServer] = {}
|
||||
print(f"[relay] dynamic watching adb forwards -> {target_host} (poll={poll_seconds}s)", flush=True)
|
||||
while True:
|
||||
try:
|
||||
target_ports = await _adb_forward_ports()
|
||||
except Exception as exc:
|
||||
print(f"[relay] adb listing failed: {exc}", file=sys.stderr, flush=True)
|
||||
await asyncio.sleep(poll_seconds)
|
||||
continue
|
||||
|
||||
for port in target_ports - set(active):
|
||||
try:
|
||||
srv = await asyncio.start_server(_handler(target_host, port), "127.0.0.1", port)
|
||||
active[port] = srv
|
||||
print(f"[relay] + 127.0.0.1:{port} -> {target_host}:{port}", flush=True)
|
||||
except OSError as exc:
|
||||
print(f"[relay] bind 127.0.0.1:{port} failed: {exc}", file=sys.stderr, flush=True)
|
||||
|
||||
for port in set(active) - target_ports:
|
||||
srv = active.pop(port)
|
||||
srv.close()
|
||||
try:
|
||||
await srv.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
print(f"[relay] - 127.0.0.1:{port}", flush=True)
|
||||
|
||||
await asyncio.sleep(poll_seconds)
|
||||
|
||||
|
||||
def _usage() -> None:
|
||||
print(__doc__, file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
async def _main() -> None:
|
||||
argv = sys.argv[1:]
|
||||
if not argv:
|
||||
_usage()
|
||||
if argv[0] == "--watch-adb":
|
||||
if len(argv) != 2:
|
||||
_usage()
|
||||
await run_dynamic(argv[1])
|
||||
else:
|
||||
if len(argv) != 3:
|
||||
_usage()
|
||||
await run_static(int(argv[0]), argv[1], int(argv[2]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
Reference in New Issue
Block a user