Files
halobestie-clone/.dev/wsl_emulator_bridge.ps1
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

170 lines
6.0 KiB
PowerShell

<#
.SYNOPSIS
Toggle a Windows-side TCP bridge so Maestro (running in WSL) can reach the
Android emulators' adb ports.
.DESCRIPTION
Android emulators on the Windows host bind their adb ports (5555, 5557) to
127.0.0.1 only. Maestro's bundled `dadb` connects directly to that port —
not via the adb server — so `ADB_SERVER_SOCKET` is irrelevant. To run
`maestro test` from inside WSL, we expose those ports on the WSL-facing
interface and let the matching WSL-side relay (.dev/wsl_tcp_relay.py)
bridge 127.0.0.1 in WSL → <gateway> on Windows.
Heads-up: idle `netsh portproxy` entries with no live target have been
reported to occasionally pin CPU on iphlpsvc. Keep these rules OFF when
you're not running Maestro from WSL — that is the whole point of this
script.
.PARAMETER Action
up Add port-proxy entries + firewall rules.
down Remove them.
status Print current portproxy + matching firewall rules.
.PARAMETER ListenAddress
Optional. Defaults to the IPv4 of the `vEthernet (WSL)` adapter — the IP
that WSL2 sees as its default gateway. Override only if your WSL adapter
is named differently.
.PARAMETER Ports
Optional. Defaults to 5554,5555,5556,5557 — the console + adb ports for
emulator-5554 (5554 console, 5555 adb) and emulator-5556 (5556 console,
5557 adb). Maestro / dadb need both: the console port for authentication
and the adb port for the actual protocol.
.EXAMPLE
# From an elevated PowerShell, before `maestro test ...` in WSL:
.\wsl_emulator_bridge.ps1 up
.EXAMPLE
# When done testing — tear it back down to avoid the CPU-pin risk:
.\wsl_emulator_bridge.ps1 down
.EXAMPLE
.\wsl_emulator_bridge.ps1 status
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet('up', 'down', 'status')]
[string]$Action,
[string]$ListenAddress,
[int[]]$Ports = @(5554, 5555, 5556, 5557)
)
$ErrorActionPreference = 'Stop'
$FirewallRulePrefix = 'WSL adb bridge'
function Assert-Admin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'This script must be run from an elevated PowerShell (Run as administrator).'
}
}
function Resolve-WslGatewayIp {
param([string]$Override)
if ($Override) { return $Override }
# WSL2 always creates a `vEthernet (WSL)` adapter on the Windows host.
# Its IPv4 address is what WSL2 sees as `default via ...` in `ip route`.
$candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
Where-Object {
$_.InterfaceAlias -like 'vEthernet (WSL*)' -and $_.PrefixOrigin -ne 'WellKnown'
} |
Sort-Object -Property InterfaceMetric
if (-not $candidates) {
throw "Couldn't find a `vEthernet (WSL)` adapter. Pass -ListenAddress explicitly."
}
return $candidates[0].IPAddress
}
function Get-PortProxyEntries {
$out = & netsh interface portproxy show v4tov4 2>&1
if ($LASTEXITCODE -ne 0) {
throw "netsh interface portproxy show v4tov4 failed: $out"
}
return $out
}
function Invoke-PortProxyUp {
param([string]$Addr, [int]$Port)
& netsh interface portproxy add v4tov4 `
listenaddress=$Addr listenport=$Port `
connectaddress=127.0.0.1 connectport=$Port | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "Failed to add portproxy for $Addr`:$Port (exit $LASTEXITCODE)"
}
$ruleName = "$FirewallRulePrefix $Port"
$existing = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if (-not $existing) {
New-NetFirewallRule -DisplayName $ruleName `
-Direction Inbound -Action Allow -Protocol TCP `
-LocalPort $Port -LocalAddress $Addr | Out-Null
}
Write-Host " + $Addr`:$Port -> 127.0.0.1:$Port (firewall: $ruleName)" -ForegroundColor Green
}
function Invoke-PortProxyDown {
param([string]$Addr, [int]$Port)
& netsh interface portproxy delete v4tov4 `
listenaddress=$Addr listenport=$Port 2>$null | Out-Null
$ruleName = "$FirewallRulePrefix $Port"
$rule = Get-NetFirewallRule -DisplayName $ruleName -ErrorAction SilentlyContinue
if ($rule) { Remove-NetFirewallRule -DisplayName $ruleName }
Write-Host " - $Addr`:$Port removed" -ForegroundColor Yellow
}
switch ($Action) {
'up' {
Assert-Admin
$addr = Resolve-WslGatewayIp -Override $ListenAddress
Write-Host "Bringing WSL adb bridge UP on $addr ..." -ForegroundColor Cyan
foreach ($p in $Ports) { Invoke-PortProxyUp -Addr $addr -Port $p }
Write-Host ''
Write-Host 'Remember to take it DOWN when you are done:' -ForegroundColor DarkGray
Write-Host ' .\wsl_emulator_bridge.ps1 down' -ForegroundColor DarkGray
}
'down' {
Assert-Admin
# If user didn't pass -ListenAddress, try to clean up whichever address
# is currently bound. Walk the existing portproxy entries.
$entries = Get-PortProxyEntries
$matched = @()
foreach ($p in $Ports) {
$regex = "^\s*(\d+\.\d+\.\d+\.\d+)\s+$p\s+"
foreach ($line in $entries) {
if ($line -match $regex) {
$matched += [pscustomobject]@{ Addr = $matches[1]; Port = $p }
}
}
}
if (-not $matched -and $ListenAddress) {
foreach ($p in $Ports) {
$matched += [pscustomobject]@{ Addr = $ListenAddress; Port = $p }
}
}
if (-not $matched) {
Write-Host 'No matching portproxy entries found. Nothing to remove.' -ForegroundColor DarkGray
break
}
Write-Host 'Tearing WSL adb bridge DOWN ...' -ForegroundColor Cyan
foreach ($m in $matched) { Invoke-PortProxyDown -Addr $m.Addr -Port $m.Port }
}
'status' {
Write-Host 'Active portproxy entries:' -ForegroundColor Cyan
Get-PortProxyEntries | ForEach-Object { Write-Host " $_" }
Write-Host ''
Write-Host 'Matching firewall rules:' -ForegroundColor Cyan
Get-NetFirewallRule -DisplayName "$FirewallRulePrefix *" -ErrorAction SilentlyContinue |
Select-Object DisplayName, Enabled, Direction, Action |
Format-Table -AutoSize | Out-String | Write-Host
}
}