Phase 3 scaffold: chat engine (WebSocket, FCM, pricing, timer, extension, history)
- Backend: WebSocket plugin, chat/pricing/timer/extension/closure/notification services - Client app: ChatBloc, pricing dialog, chat screen with message status, extension/goodbye flow, history - Mitra app: MitraChatBloc, ExtensionBloc, chat screen, extension accept/reject, history - Control center: free trial, extension timeout, early end config toggles - DB migration: chat_messages, session_closures, session_extensions, customer_transactions tables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,16 @@ abstract class PairingEvent extends Equatable {
|
||||
}
|
||||
|
||||
class RequestPairing extends PairingEvent {}
|
||||
|
||||
class RequestPairingWithTier extends PairingEvent {
|
||||
final int? durationMinutes;
|
||||
final int? price;
|
||||
final bool isFreeTrial;
|
||||
RequestPairingWithTier({this.durationMinutes, this.price, this.isFreeTrial = false});
|
||||
@override
|
||||
List<Object?> get props => [durationMinutes, price, isFreeTrial];
|
||||
}
|
||||
|
||||
class CancelPairing extends PairingEvent {}
|
||||
|
||||
class _PairingStatusUpdate extends PairingEvent {
|
||||
@@ -71,29 +81,42 @@ class PairingBloc extends Bloc<PairingEvent, PairingState> {
|
||||
|
||||
PairingBloc({required this.apiClient}) : super(PairingInitial()) {
|
||||
on<RequestPairing>(_onRequestPairing);
|
||||
on<RequestPairingWithTier>(_onRequestPairingWithTier);
|
||||
on<CancelPairing>(_onCancelPairing);
|
||||
on<_PairingStatusUpdate>(_onStatusUpdate);
|
||||
on<_PairingTimeout>(_onTimeout);
|
||||
}
|
||||
|
||||
Future<void> _onRequestPairing(RequestPairing event, Emitter<PairingState> emit) async {
|
||||
// Reset to initial so BlocListener can detect new errors
|
||||
await _doPairingRequest(emit, {});
|
||||
}
|
||||
|
||||
Future<void> _onRequestPairingWithTier(RequestPairingWithTier event, Emitter<PairingState> emit) async {
|
||||
final body = <String, dynamic>{};
|
||||
if (event.isFreeTrial) {
|
||||
body['is_free_trial'] = true;
|
||||
} else {
|
||||
body['duration_minutes'] = event.durationMinutes;
|
||||
body['price'] = event.price;
|
||||
}
|
||||
await _doPairingRequest(emit, body);
|
||||
}
|
||||
|
||||
Future<void> _doPairingRequest(Emitter<PairingState> emit, Map<String, dynamic> body) async {
|
||||
if (state is! PairingInitial) {
|
||||
emit(PairingInitial());
|
||||
}
|
||||
try {
|
||||
final response = await apiClient.post('/api/client/chat/request');
|
||||
final response = await apiClient.post('/api/client/chat/request', data: body);
|
||||
final data = response['data'] as Map<String, dynamic>;
|
||||
final sessionId = data['id'] as String;
|
||||
|
||||
emit(PairingSearching(sessionId));
|
||||
|
||||
// Start 60s local timeout as a safety net
|
||||
_timeoutTimer = Timer(const Duration(seconds: 60), () {
|
||||
add(_PairingTimeout());
|
||||
});
|
||||
|
||||
// Listen to SSE for status updates
|
||||
_listenToSSE(sessionId);
|
||||
} on DioException catch (e) {
|
||||
final code = e.response?.data?['error']?['code'];
|
||||
@@ -101,6 +124,8 @@ class PairingBloc extends Bloc<PairingEvent, PairingState> {
|
||||
emit(PairingNoBestie());
|
||||
} else if (code == 'ALREADY_ACTIVE') {
|
||||
emit(PairingError('Kamu sudah memiliki sesi aktif.'));
|
||||
} else if (code == 'FREE_TRIAL_INELIGIBLE') {
|
||||
emit(PairingError('Kamu tidak memenuhi syarat untuk free trial.'));
|
||||
} else {
|
||||
emit(PairingError('Gagal memulai. Coba lagi.'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user