Phase 3.1: Complete mitra_app Riverpod migration (all blocs, fix auth bug)

- Migrate AuthBloc → MitraAuthNotifier (fixes stuck-loading bug: now returns
  MitraAuthInitialData when currentUser is null)
- Migrate StatusBloc → OnlineStatusNotifier (heartbeat timer + lifecycle)
- Migrate ExtensionBloc → MitraExtensionNotifier (accept/reject + goodbye)
- Migrate ChatRequestBloc → ChatRequestNotifier (WebSocket incoming requests)
- Migrate MitraChatBloc → MitraChatNotifier (WebSocket chat + messages)
- Update router to use Riverpod auth state for redirects
- Remove all flutter_bloc usage from mitra_app screens and main.dart
- MultiBlocProvider fully removed from mitra_app

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 14:08:45 +08:00
parent bc66bbf50a
commit 35d470b851
17 changed files with 1298 additions and 461 deletions

View File

@@ -0,0 +1,97 @@
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../api/api_client_provider.dart';
part 'status_notifier.g.dart';
// States
sealed class OnlineStatusData {
const OnlineStatusData();
}
class StatusInitialData extends OnlineStatusData {
const StatusInitialData();
}
class StatusLoadedData extends OnlineStatusData {
final bool isOnline;
const StatusLoadedData({required this.isOnline});
}
class StatusLoadingData extends OnlineStatusData {
const StatusLoadingData();
}
class StatusErrorData extends OnlineStatusData {
final String message;
const StatusErrorData(this.message);
}
@Riverpod(keepAlive: true)
class OnlineStatus extends _$OnlineStatus {
Timer? _heartbeatTimer;
@override
OnlineStatusData build() => const StatusInitialData();
Future<void> load() async {
try {
final response = await ref.read(apiClientProvider).get('/api/mitra/status');
final data = response['data'] as Map<String, dynamic>;
state = StatusLoadedData(isOnline: data['is_online'] as bool);
} catch (e) {
state = const StatusLoadedData(isOnline: false);
}
}
Future<void> toggleOnline() async {
state = const StatusLoadingData();
try {
await ref.read(apiClientProvider).post('/api/mitra/status/online');
_startHeartbeat();
state = const StatusLoadedData(isOnline: true);
} catch (e) {
state = const StatusErrorData('Gagal mengubah status. Coba lagi.');
}
}
Future<void> toggleOffline() async {
state = const StatusLoadingData();
try {
await ref.read(apiClientProvider).post('/api/mitra/status/offline');
_stopHeartbeat();
state = const StatusLoadedData(isOnline: false);
} catch (e) {
state = const StatusErrorData('Gagal mengubah status. Coba lagi.');
}
}
void onAppPaused() {
_stopHeartbeat();
}
void onAppResumed() {
if (state is StatusLoadedData && (state as StatusLoadedData).isOnline) {
_startHeartbeat();
}
load();
}
void _startHeartbeat() {
_stopHeartbeat();
_heartbeatTimer = Timer.periodic(const Duration(seconds: 15), (_) {
_heartbeatTick();
});
}
void _stopHeartbeat() {
_heartbeatTimer?.cancel();
_heartbeatTimer = null;
}
Future<void> _heartbeatTick() async {
try {
await ref.read(apiClientProvider).post('/api/mitra/status/heartbeat');
} catch (_) {}
}
}

View File

@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'status_notifier.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$onlineStatusHash() => r'6b42328eaba0f7934b0e3eaa54eb6b764f1c4e53';
/// See also [OnlineStatus].
@ProviderFor(OnlineStatus)
final onlineStatusProvider =
NotifierProvider<OnlineStatus, OnlineStatusData>.internal(
OnlineStatus.new,
name: r'onlineStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$onlineStatusHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$OnlineStatus = Notifier<OnlineStatusData>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package