Backend - payment_sessions → payment_requests rename across DB schema + 29 files - payment.service.js becomes product-agnostic owner: EventEmitter + Xendit wrapper + requestPayment / confirmPayment public API; legacy aliases retained for existing chat callers - Webhook handler at POST /api/shared/payment/webhooks/xendit, with constant-time token verification (8 vitest cases) - Server-driven pairing: payment.service emits payment_request.confirmed → pairing subscriber starts the blast. Legacy POST /chat/request still works during the cutover. - Reconciliation sweeper extended (re-emits events for confirmed rows with no chat session) - SIGTERM drain + startup reconciliation pass in server.js Customer app - waiting_payment_screen opens xendit_invoice_url via LaunchMode.inAppBrowserView - searching / no-bestie / targeted-waiting / pairing-notifier updated to consume the new payment_request_id contract - pending_payments_provider + bestie-unavailable dialog migrated Dev / testing - XENDIT_ENABLED=false is the safe default; .env.example documents the four new vars - backend/.dev/xendit-fake-webhook.sh exercises the handler without ngrok - 90/92 backend tests pass (two pre-existing session-timer flakes, unrelated); client_app analyzer clean - requirement/phase5-xendit-plan.md is the canonical reference Stage 8 (live E2E) blocked on Xendit test-mode keys. The dashboard's single-webhook-URL constraint will be worked around via a self-poll script next session. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
68 lines
2.5 KiB
Dart
68 lines
2.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import '../../../core/pairing/pairing_notifier.dart';
|
|
|
|
/// Terminal failed-pairing screen.
|
|
///
|
|
/// Reached when the pairing notifier transitions to [PairingFailedData]
|
|
/// (terminal — payment session is `failed_delivery` server-side, audit row
|
|
/// recorded). Copy is intentionally identical regardless of `cause_tag` for
|
|
/// now (the design pass will revise this later).
|
|
///
|
|
/// Single CTA "Kembali ke beranda" resets the pairing notifier and routes
|
|
/// home. PopScope falls back to home for deep-link entry per project memory
|
|
/// rule "Deep-link pop fallback".
|
|
class NoBestieScreen extends ConsumerWidget {
|
|
const NoBestieScreen({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
return PopScope(
|
|
canPop: true,
|
|
onPopInvokedWithResult: (didPop, _) {
|
|
if (!didPop) return;
|
|
ref.read(pairingProvider.notifier).reset();
|
|
},
|
|
child: Scaffold(
|
|
body: SafeArea(
|
|
child: Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(Icons.sentiment_dissatisfied, size: 80, color: Colors.orange),
|
|
const SizedBox(height: 24),
|
|
const Text(
|
|
'Belum berhasil terhubung',
|
|
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
|
|
),
|
|
const SizedBox(height: 12),
|
|
const Text(
|
|
'Maaf, kami tidak bisa menemukan bestie untuk sesimu. '
|
|
'Tim kami akan menghubungimu segera.',
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(fontSize: 16, color: Colors.grey),
|
|
),
|
|
const SizedBox(height: 48),
|
|
ElevatedButton(
|
|
style: ElevatedButton.styleFrom(
|
|
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
|
|
),
|
|
onPressed: () {
|
|
ref.read(pairingProvider.notifier).reset();
|
|
context.go('/home');
|
|
},
|
|
child: const Text('Kembali ke beranda'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|