Phase 4 Stage 5: pairing UX upgrades (searching + match + targeted-wait)

Searching screen: soft-prompt card reskin, pulsing-dots panel replaces
the spinner, inline 5-min timeout panel with `coba cari lagi` (resets
pairing notifier + routes to /payment/entry for a fresh funnel — the
server-side payment is failed_pairing at that point so a stale retry
isn't valid) and `kembali ke home` ghost CTA.

Bestie-found screen: S9 Match-V4 reskin — HaloOrb + status dot +
'halo, aku bestie {name}' + `mulai sesi {N} menit →` with N pulled from
the active session's duration_minutes.

Targeted-wait overlay (new) at /chat/waiting-targeted/:mitraId. Three
sub-states from pairingProvider's PairingTargetedWaitingData:
waiting (20s countdown) / accepted (routes to chat) / declined (stubbed
BestieOfflinePopup with a TODO pointing to Stage 8). Reached via
payment_screen._routeToSearchOnConfirmed when the confirm carried a
targetedMitraId — keeps the mandatory payment-before-pairing invariant.

Dev-only POST /internal/_test/force-pairing-timeout drives the 5-min
timeout shortcut for the Maestro flow without waiting live.

Maestro 05_searching_timeout.yaml + force_pairing_timeout.js helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-10 16:49:07 +08:00
parent 7ae8f33b2c
commit f170d54535
8 changed files with 800 additions and 93 deletions

View File

@@ -6,7 +6,9 @@
// test phone numbers or fixed codes into production code paths.
import { peekStubOtp } from '../../services/otp.service.js'
import { expirePairingRequest } from '../../services/pairing.service.js'
import { getDb } from '../../db/client.js'
import { PairingFailureCause, SessionStatus } from '../../constants.js'
const sql = getDb()
@@ -77,4 +79,33 @@ export const internalTestRoutes = async (fastify) => {
}
return { ok: true, ...updated }
})
// Force-expire a pairing blast (used by Maestro Stage 5 flow to drive the
// searching screen into the timeout state without waiting 5 minutes). Marks
// the most-recently-created blast chat_session as no_mitra_available.
//
// Body shape:
// { session_id: '<uuid>' } → expire this specific session
// { latest: true } → expire the most-recent SEARCHING session
fastify.post('/force-pairing-timeout', async (request, reply) => {
const { session_id, latest } = request.body ?? {}
let target = session_id
if (latest === true) {
const [row] = await sql`
SELECT id FROM chat_sessions
WHERE status = ${SessionStatus.SEARCHING}
ORDER BY created_at DESC
LIMIT 1
`
if (!row) {
return reply.code(404).send({ error: 'no_searching_session' })
}
target = row.id
}
if (!target) {
return reply.code(400).send({ error: 'session_id or latest:true required in body' })
}
await expirePairingRequest(target, PairingFailureCause.NO_MITRA_AVAILABLE)
return { ok: true, session_id: target }
})
}