Change PIN to 1451
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
55
apps/captain-mobile-v2/flutter/lib/services/api_service.dart
Normal file
55
apps/captain-mobile-v2/flutter/lib/services/api_service.dart
Normal file
@@ -0,0 +1,55 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../config/api_config.dart';
|
||||
import '../models/conversation.dart';
|
||||
import '../models/message.dart';
|
||||
|
||||
class ApiService {
|
||||
String? _token;
|
||||
|
||||
void setToken(String? token) {
|
||||
_token = token;
|
||||
}
|
||||
|
||||
Map<String, String> get _headers => {
|
||||
'Content-Type': 'application/json',
|
||||
if (_token != null) 'Authorization': 'Bearer $_token',
|
||||
};
|
||||
|
||||
Future<List<Conversation>> getConversations() async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}/conversations'),
|
||||
headers: _headers,
|
||||
).timeout(ApiConfig.connectionTimeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => Conversation.fromJson(e)).toList();
|
||||
}
|
||||
throw Exception('Failed to load conversations');
|
||||
}
|
||||
|
||||
Future<List<Message>> getConversationMessages(String conversationId) async {
|
||||
final response = await http.get(
|
||||
Uri.parse('${ApiConfig.baseUrl}/conversations/$conversationId'),
|
||||
headers: _headers,
|
||||
).timeout(ApiConfig.connectionTimeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final List<dynamic> data = jsonDecode(response.body);
|
||||
return data.map((e) => Message.fromJson(e)).toList();
|
||||
}
|
||||
throw Exception('Failed to load messages');
|
||||
}
|
||||
|
||||
Future<void> deleteConversation(String conversationId) async {
|
||||
final response = await http.delete(
|
||||
Uri.parse('${ApiConfig.baseUrl}/conversations/$conversationId'),
|
||||
headers: _headers,
|
||||
).timeout(ApiConfig.connectionTimeout);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Failed to delete conversation');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../config/api_config.dart';
|
||||
|
||||
class AuthService {
|
||||
static const String _tokenKey = 'auth_token';
|
||||
static const String _expiresKey = 'token_expires';
|
||||
static const String _usernameKey = 'username';
|
||||
|
||||
String? _token;
|
||||
DateTime? _expiresAt;
|
||||
String? _username;
|
||||
|
||||
String? get token => _token;
|
||||
String? get username => _username;
|
||||
bool get isAuthenticated => _token != null && !isExpired;
|
||||
bool get isExpired =>
|
||||
_expiresAt != null && DateTime.now().isAfter(_expiresAt!);
|
||||
|
||||
Future<void> loadStoredAuth() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_token = prefs.getString(_tokenKey);
|
||||
_username = prefs.getString(_usernameKey);
|
||||
final expiresStr = prefs.getString(_expiresKey);
|
||||
if (expiresStr != null) {
|
||||
_expiresAt = DateTime.tryParse(expiresStr);
|
||||
}
|
||||
}
|
||||
|
||||
String? lastError;
|
||||
|
||||
Future<bool> login(String username, String password) async {
|
||||
lastError = null;
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('${ApiConfig.baseUrl}/auth/login'),
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({'username': username, 'password': password}),
|
||||
).timeout(ApiConfig.connectionTimeout);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = jsonDecode(response.body);
|
||||
_token = data['token'];
|
||||
_expiresAt = DateTime.parse(data['expires_at']);
|
||||
_username = username;
|
||||
|
||||
// Save to storage
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_tokenKey, _token!);
|
||||
await prefs.setString(_expiresKey, _expiresAt!.toIso8601String());
|
||||
await prefs.setString(_usernameKey, _username!);
|
||||
|
||||
return true;
|
||||
}
|
||||
lastError = 'Invalid credentials (${response.statusCode})';
|
||||
return false;
|
||||
} catch (e) {
|
||||
lastError = 'Connection error: $e';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_token = null;
|
||||
_expiresAt = null;
|
||||
_username = null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_tokenKey);
|
||||
await prefs.remove(_expiresKey);
|
||||
await prefs.remove(_usernameKey);
|
||||
}
|
||||
}
|
||||
223
apps/captain-mobile-v2/flutter/lib/services/chat_service.dart
Normal file
223
apps/captain-mobile-v2/flutter/lib/services/chat_service.dart
Normal file
@@ -0,0 +1,223 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
import '../config/api_config.dart';
|
||||
|
||||
enum ChatConnectionState {
|
||||
disconnected,
|
||||
connecting,
|
||||
connected,
|
||||
reconnecting,
|
||||
error,
|
||||
}
|
||||
|
||||
/// Chat session info (v3 - dynamic sessions)
|
||||
class ChatSession {
|
||||
final String sessionId;
|
||||
final String name;
|
||||
final String? createdAt;
|
||||
|
||||
ChatSession({
|
||||
required this.sessionId,
|
||||
required this.name,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory ChatSession.fromJson(Map<String, dynamic> json) {
|
||||
return ChatSession(
|
||||
sessionId: json['session_id'] ?? '',
|
||||
name: json['name'] ?? 'Session',
|
||||
createdAt: json['created_at'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Chat service that connects to Claude sessions (v3)
|
||||
class ChatService {
|
||||
WebSocketChannel? _channel;
|
||||
String? _token;
|
||||
|
||||
final _messagesController = StreamController<Map<String, dynamic>>.broadcast();
|
||||
final _stateController = StreamController<ChatConnectionState>.broadcast();
|
||||
final _errorController = StreamController<String>.broadcast();
|
||||
|
||||
ChatConnectionState _currentState = ChatConnectionState.disconnected;
|
||||
int _reconnectAttempts = 0;
|
||||
Timer? _reconnectTimer;
|
||||
Timer? _pingTimer;
|
||||
bool _intentionalDisconnect = false;
|
||||
|
||||
Stream<Map<String, dynamic>> get messages => _messagesController.stream;
|
||||
Stream<ChatConnectionState> get connectionState => _stateController.stream;
|
||||
Stream<String> get errors => _errorController.stream;
|
||||
ChatConnectionState get currentState => _currentState;
|
||||
|
||||
void setToken(String? token) {
|
||||
_token = token;
|
||||
}
|
||||
|
||||
void _setState(ChatConnectionState state) {
|
||||
_currentState = state;
|
||||
_stateController.add(state);
|
||||
}
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_token == null) return;
|
||||
if (_currentState == ChatConnectionState.connecting) return;
|
||||
|
||||
_intentionalDisconnect = false;
|
||||
_setState(ChatConnectionState.connecting);
|
||||
|
||||
try {
|
||||
_channel?.sink.close();
|
||||
_channel = WebSocketChannel.connect(
|
||||
Uri.parse('${ApiConfig.wsUrl}/ws/chat'),
|
||||
);
|
||||
|
||||
// Send auth token immediately
|
||||
_channel!.sink.add(jsonEncode({'token': _token}));
|
||||
|
||||
_channel!.stream.listen(
|
||||
(data) {
|
||||
try {
|
||||
final message = jsonDecode(data);
|
||||
_handleMessage(message);
|
||||
} catch (e) {
|
||||
_errorController.add('Failed to parse message: $e');
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
_errorController.add('WebSocket error: $error');
|
||||
_setState(ChatConnectionState.error);
|
||||
if (!_intentionalDisconnect) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
_setState(ChatConnectionState.disconnected);
|
||||
if (!_intentionalDisconnect) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
},
|
||||
cancelOnError: false,
|
||||
);
|
||||
} catch (e) {
|
||||
_errorController.add('Connection failed: $e');
|
||||
_setState(ChatConnectionState.error);
|
||||
if (!_intentionalDisconnect) {
|
||||
_scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleMessage(Map<String, dynamic> message) {
|
||||
final type = message['type'];
|
||||
|
||||
switch (type) {
|
||||
case 'init':
|
||||
// Initial state with sessions list
|
||||
_setState(ChatConnectionState.connected);
|
||||
_reconnectAttempts = 0;
|
||||
_startPingTimer();
|
||||
_messagesController.add(message);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
final errorMsg = message['message'] ?? message['content'] ?? 'Unknown error';
|
||||
_errorController.add(errorMsg);
|
||||
break;
|
||||
|
||||
case 'pong':
|
||||
break;
|
||||
|
||||
default:
|
||||
// Forward all other messages (output, done, session_connected, etc.)
|
||||
_messagesController.add(message);
|
||||
}
|
||||
}
|
||||
|
||||
void _startPingTimer() {
|
||||
_pingTimer?.cancel();
|
||||
_pingTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (_currentState == ChatConnectionState.connected) {
|
||||
sendRaw({'type': 'ping'});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _scheduleReconnect() {
|
||||
if (_intentionalDisconnect) return;
|
||||
if (_reconnectAttempts >= ApiConfig.maxReconnectAttempts) {
|
||||
_errorController.add('Max reconnection attempts reached');
|
||||
return;
|
||||
}
|
||||
|
||||
_reconnectTimer?.cancel();
|
||||
final delay = Duration(
|
||||
seconds: ApiConfig.reconnectDelay.inSeconds * (1 << _reconnectAttempts),
|
||||
);
|
||||
_reconnectAttempts++;
|
||||
_setState(ChatConnectionState.reconnecting);
|
||||
|
||||
_reconnectTimer = Timer(delay, () {
|
||||
if (_token != null && !_intentionalDisconnect) {
|
||||
connect();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a new chat session
|
||||
void createSession(String name) {
|
||||
sendRaw({
|
||||
'type': 'create_session',
|
||||
'name': name,
|
||||
});
|
||||
}
|
||||
|
||||
/// Connect to an existing session by session_id
|
||||
void connectToSession(String sessionId) {
|
||||
sendRaw({
|
||||
'type': 'connect_session',
|
||||
'session_id': sessionId,
|
||||
});
|
||||
}
|
||||
|
||||
/// Send message to connected session
|
||||
void sendMessage(String content) {
|
||||
if (_currentState != ChatConnectionState.connected) {
|
||||
_errorController.add('Not connected');
|
||||
return;
|
||||
}
|
||||
|
||||
sendRaw({
|
||||
'type': 'message',
|
||||
'content': content,
|
||||
});
|
||||
}
|
||||
|
||||
/// Request sessions list
|
||||
void listSessions() {
|
||||
sendRaw({'type': 'list_sessions'});
|
||||
}
|
||||
|
||||
void sendRaw(Map<String, dynamic> data) {
|
||||
if (_channel != null) {
|
||||
_channel!.sink.add(jsonEncode(data));
|
||||
}
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
_intentionalDisconnect = true;
|
||||
_pingTimer?.cancel();
|
||||
_reconnectTimer?.cancel();
|
||||
_channel?.sink.close();
|
||||
_setState(ChatConnectionState.disconnected);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
disconnect();
|
||||
_messagesController.close();
|
||||
_stateController.close();
|
||||
_errorController.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user