Phase 3.1: Remove flutter_bloc + equatable, delete old bloc files
- Remove flutter_bloc and equatable dependencies from both apps - Delete all 10 old bloc files (5 per app) - Fix 6 remaining screens that used context.read<ApiClient>() from flutter_bloc → converted to ConsumerStatefulWidget/ConsumerWidget with ref.read(apiClientProvider) - Both apps now use Riverpod exclusively for state management Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../api/api_client.dart';
|
||||
import '../constants.dart';
|
||||
|
||||
// Events
|
||||
abstract class PairingEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
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 {
|
||||
final Map<String, dynamic> data;
|
||||
_PairingStatusUpdate(this.data);
|
||||
@override
|
||||
List<Object?> get props => [data];
|
||||
}
|
||||
|
||||
class _PairingTimeout extends PairingEvent {}
|
||||
class _ConnectionError extends PairingEvent {}
|
||||
|
||||
// States
|
||||
abstract class PairingState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class PairingInitial extends PairingState {}
|
||||
class PairingSearching extends PairingState {
|
||||
final String sessionId;
|
||||
PairingSearching(this.sessionId);
|
||||
@override
|
||||
List<Object?> get props => [sessionId];
|
||||
}
|
||||
|
||||
class PairingBestieFound extends PairingState {
|
||||
final String sessionId;
|
||||
final String mitraName;
|
||||
PairingBestieFound({required this.sessionId, required this.mitraName});
|
||||
@override
|
||||
List<Object?> get props => [sessionId, mitraName];
|
||||
}
|
||||
|
||||
class PairingActive extends PairingState {
|
||||
final String sessionId;
|
||||
final String mitraName;
|
||||
PairingActive({required this.sessionId, required this.mitraName});
|
||||
@override
|
||||
List<Object?> get props => [sessionId, mitraName];
|
||||
}
|
||||
|
||||
class PairingNoBestie extends PairingState {}
|
||||
class PairingCancelled extends PairingState {}
|
||||
|
||||
class PairingError extends PairingState {
|
||||
final String message;
|
||||
PairingError(this.message);
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
// Bloc
|
||||
class PairingBloc extends Bloc<PairingEvent, PairingState> {
|
||||
final ApiClient apiClient;
|
||||
Timer? _timeoutTimer;
|
||||
WebSocketChannel? _channel;
|
||||
StreamSubscription? _wsSubscription;
|
||||
|
||||
PairingBloc({required this.apiClient}) : super(PairingInitial()) {
|
||||
on<RequestPairing>(_onRequestPairing);
|
||||
on<RequestPairingWithTier>(_onRequestPairingWithTier);
|
||||
on<CancelPairing>(_onCancelPairing);
|
||||
on<_PairingStatusUpdate>(_onStatusUpdate);
|
||||
on<_PairingTimeout>(_onTimeout);
|
||||
on<_ConnectionError>(_onConnectionError);
|
||||
}
|
||||
|
||||
Future<void> _onRequestPairing(RequestPairing event, Emitter<PairingState> emit) async {
|
||||
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 {
|
||||
// Connect to WebSocket first to listen for pairing status
|
||||
await _connectWebSocket();
|
||||
|
||||
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));
|
||||
|
||||
_timeoutTimer = Timer(const Duration(seconds: 60), () {
|
||||
add(_PairingTimeout());
|
||||
});
|
||||
} on DioException catch (e) {
|
||||
_cleanup();
|
||||
final code = e.response?.data?['error']?['code'];
|
||||
if (code == 'NO_MITRA_AVAILABLE') {
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _connectWebSocket() async {
|
||||
_closeWebSocket();
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
final token = await user.getIdToken();
|
||||
final wsUrl = ApiClient.baseUrl
|
||||
.replaceFirst('https://', 'wss://')
|
||||
.replaceFirst('http://', 'ws://');
|
||||
|
||||
_channel = WebSocketChannel.connect(Uri.parse('$wsUrl/api/shared/ws'));
|
||||
|
||||
_wsSubscription = _channel!.stream.listen(
|
||||
(raw) {
|
||||
final data = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
if (data['type'] == WsMessage.authOk) return;
|
||||
add(_PairingStatusUpdate(data));
|
||||
},
|
||||
onError: (_) => add(_ConnectionError()),
|
||||
onDone: () => add(_ConnectionError()),
|
||||
);
|
||||
|
||||
// Authenticate without session_id — just for receiving pairing status
|
||||
_channel!.sink.add(jsonEncode({
|
||||
'type': WsMessage.auth,
|
||||
'token': token,
|
||||
}));
|
||||
}
|
||||
|
||||
Future<void> _onConnectionError(_ConnectionError event, Emitter<PairingState> emit) async {
|
||||
// WebSocket disconnected during pairing — stay in current state,
|
||||
// FCM will still deliver notifications
|
||||
}
|
||||
|
||||
Future<void> _onStatusUpdate(_PairingStatusUpdate event, Emitter<PairingState> emit) async {
|
||||
final data = event.data;
|
||||
final type = data['type'] as String?;
|
||||
|
||||
if (type == WsMessage.paired) {
|
||||
_cleanup();
|
||||
final mitraName = data['mitra_display_name'] as String? ?? 'Bestie';
|
||||
final sessionId = data['session_id'] as String;
|
||||
emit(PairingBestieFound(sessionId: sessionId, mitraName: mitraName));
|
||||
|
||||
// Brief delay then transition to active
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
emit(PairingActive(sessionId: sessionId, mitraName: mitraName));
|
||||
} else if (type == SessionStatus.expired) {
|
||||
_cleanup();
|
||||
emit(PairingNoBestie());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onCancelPairing(CancelPairing event, Emitter<PairingState> emit) async {
|
||||
if (state is PairingSearching) {
|
||||
final sessionId = (state as PairingSearching).sessionId;
|
||||
try {
|
||||
await apiClient.post('/api/client/chat/request/$sessionId/cancel');
|
||||
} catch (_) {}
|
||||
_cleanup();
|
||||
emit(PairingCancelled());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onTimeout(_PairingTimeout event, Emitter<PairingState> emit) async {
|
||||
_cleanup();
|
||||
emit(PairingNoBestie());
|
||||
}
|
||||
|
||||
void _closeWebSocket() {
|
||||
_wsSubscription?.cancel();
|
||||
_wsSubscription = null;
|
||||
_channel?.sink.close();
|
||||
_channel = null;
|
||||
}
|
||||
|
||||
void _cleanup() {
|
||||
_timeoutTimer?.cancel();
|
||||
_timeoutTimer = null;
|
||||
_closeWebSocket();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_cleanup();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user