All checks were successful
Create Release / build (push) Successful in 6s
Server-side: - Add push credential auto-creation for FCM incoming call notifications - Add queue alert FCM notifications (data-only for background delivery) - Add queue alert cancellation on call accept/disconnect - Fix caller ID to show caller's number instead of Twilio number - Fix FCM token storage when refresh_token is null - Add pre_call_status tracking to revert agent status 30s after call ends - Add SSE fallback polling for mobile app connectivity Mobile app: - Add Android telecom permissions and phone account registration - Add VoiceFirebaseMessagingService for incoming call push handling - Add insistent queue alert notifications with custom sound - Fix caller number display on active call screen - Add caller ID selection dropdown on dashboard - Add phone numbers endpoint and provider support - Add unit tests for CallInfo, QueueState, and CallProvider - Remove local.properties from tracking, add .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.0 KiB
Dart
109 lines
3.0 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|
import '../models/user.dart';
|
|
import 'api_client.dart';
|
|
|
|
class AuthService {
|
|
final ApiClient _api;
|
|
final FlutterSecureStorage _storage = const FlutterSecureStorage();
|
|
Timer? _refreshTimer;
|
|
|
|
AuthService(this._api);
|
|
|
|
Future<User> login(String serverUrl, String username, String password,
|
|
{String? fcmToken}) async {
|
|
await _api.setBaseUrl(serverUrl);
|
|
|
|
final response = await _api.dio.post(
|
|
'/auth/login',
|
|
data: {
|
|
'username': username,
|
|
'password': password,
|
|
if (fcmToken != null) 'fcm_token': fcmToken,
|
|
},
|
|
options: Options(receiveTimeout: const Duration(seconds: 60)),
|
|
);
|
|
|
|
final data = response.data;
|
|
if (data['success'] != true) {
|
|
throw Exception(data['message'] ?? 'Login failed');
|
|
}
|
|
|
|
await _storage.write(key: 'access_token', value: data['access_token']);
|
|
await _storage.write(key: 'refresh_token', value: data['refresh_token']);
|
|
await _storage.write(key: 'user_data', value: jsonEncode(data['user']));
|
|
|
|
_scheduleRefresh(data['expires_in'] as int? ?? 3600);
|
|
|
|
return User.fromJson(data['user']);
|
|
}
|
|
|
|
Future<User?> tryRestoreSession() async {
|
|
final token = await _storage.read(key: 'access_token');
|
|
if (token == null) return null;
|
|
|
|
await _api.restoreBaseUrl();
|
|
if (_api.dio.options.baseUrl.isEmpty) return null;
|
|
|
|
try {
|
|
final response = await _api.dio.get('/agent/status');
|
|
if (response.statusCode != 200) return null;
|
|
|
|
final userData = await _storage.read(key: 'user_data');
|
|
if (userData != null) {
|
|
return User.fromJson(jsonDecode(userData) as Map<String, dynamic>);
|
|
}
|
|
return null;
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> refreshToken() async {
|
|
final refreshToken = await _storage.read(key: 'refresh_token');
|
|
if (refreshToken == null) throw Exception('No refresh token');
|
|
|
|
final response = await _api.dio.post('/auth/refresh', data: {
|
|
'refresh_token': refreshToken,
|
|
});
|
|
|
|
final data = response.data;
|
|
if (data['success'] != true) {
|
|
throw Exception('Token refresh failed');
|
|
}
|
|
|
|
await _storage.write(key: 'access_token', value: data['access_token']);
|
|
if (data['refresh_token'] != null) {
|
|
await _storage.write(key: 'refresh_token', value: data['refresh_token']);
|
|
}
|
|
|
|
_scheduleRefresh(data['expires_in'] as int? ?? 3600);
|
|
}
|
|
|
|
void _scheduleRefresh(int expiresInSeconds) {
|
|
_refreshTimer?.cancel();
|
|
// Refresh 2 minutes before expiry
|
|
final refreshIn = Duration(seconds: expiresInSeconds - 120);
|
|
if (refreshIn.isNegative) return;
|
|
_refreshTimer = Timer(refreshIn, () async {
|
|
try {
|
|
await refreshToken();
|
|
} catch (_) {}
|
|
});
|
|
}
|
|
|
|
Future<void> logout() async {
|
|
_refreshTimer?.cancel();
|
|
try {
|
|
await _api.dio.post('/auth/logout');
|
|
} catch (_) {}
|
|
await _storage.deleteAll();
|
|
}
|
|
|
|
void dispose() {
|
|
_refreshTimer?.cancel();
|
|
}
|
|
}
|