diff --git a/lib/core/theme.dart b/lib/core/theme.dart index 8aeee30..d764cf3 100644 --- a/lib/core/theme.dart +++ b/lib/core/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; /// inou brand colors from styleguide class InouColors { @@ -149,59 +150,59 @@ class AppTheme { ), ), ), - textTheme: const TextTheme( - displayLarge: TextStyle( + textTheme: GoogleFonts.soraTextTheme().copyWith( + displayLarge: GoogleFonts.sora( fontSize: 40, fontWeight: FontWeight.w700, color: InouColors.textPrimary, ), - headlineLarge: TextStyle( + headlineLarge: GoogleFonts.sora( fontSize: 32, fontWeight: FontWeight.w700, color: InouColors.textPrimary, ), - headlineMedium: TextStyle( + headlineMedium: GoogleFonts.sora( fontSize: 22, fontWeight: FontWeight.w600, color: InouColors.textPrimary, ), - headlineSmall: TextStyle( + headlineSmall: GoogleFonts.sora( fontSize: 18, fontWeight: FontWeight.w600, color: InouColors.textPrimary, ), - titleLarge: TextStyle( + titleLarge: GoogleFonts.sora( fontSize: 16, fontWeight: FontWeight.w600, color: InouColors.textPrimary, ), - titleMedium: TextStyle( + titleMedium: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w600, color: InouColors.textPrimary, ), - bodyLarge: TextStyle( + bodyLarge: GoogleFonts.sora( fontSize: 16, fontWeight: FontWeight.w400, color: InouColors.textPrimary, ), - bodyMedium: TextStyle( + bodyMedium: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w400, color: InouColors.textPrimary, ), - bodySmall: TextStyle( + bodySmall: GoogleFonts.sora( fontSize: 12, fontWeight: FontWeight.w400, color: InouColors.textMuted, ), - labelLarge: TextStyle( + labelLarge: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w600, color: InouColors.textPrimary, letterSpacing: 0.5, ), - labelSmall: TextStyle( + labelSmall: GoogleFonts.sora( fontSize: 11, fontWeight: FontWeight.w600, color: InouColors.textMuted, @@ -322,59 +323,59 @@ class AppTheme { ), ), ), - textTheme: const TextTheme( - displayLarge: TextStyle( + textTheme: GoogleFonts.soraTextTheme().copyWith( + displayLarge: GoogleFonts.sora( fontSize: 40, fontWeight: FontWeight.w700, color: InouColors.textPrimaryDark, ), - headlineLarge: TextStyle( + headlineLarge: GoogleFonts.sora( fontSize: 32, fontWeight: FontWeight.w700, color: InouColors.textPrimaryDark, ), - headlineMedium: TextStyle( + headlineMedium: GoogleFonts.sora( fontSize: 22, fontWeight: FontWeight.w600, color: InouColors.textPrimaryDark, ), - headlineSmall: TextStyle( + headlineSmall: GoogleFonts.sora( fontSize: 18, fontWeight: FontWeight.w600, color: InouColors.textPrimaryDark, ), - titleLarge: TextStyle( + titleLarge: GoogleFonts.sora( fontSize: 16, fontWeight: FontWeight.w600, color: InouColors.textPrimaryDark, ), - titleMedium: TextStyle( + titleMedium: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w600, color: InouColors.textPrimaryDark, ), - bodyLarge: TextStyle( + bodyLarge: GoogleFonts.sora( fontSize: 16, fontWeight: FontWeight.w400, color: InouColors.textPrimaryDark, ), - bodyMedium: TextStyle( + bodyMedium: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w400, color: InouColors.textPrimaryDark, ), - bodySmall: TextStyle( + bodySmall: GoogleFonts.sora( fontSize: 12, fontWeight: FontWeight.w400, color: InouColors.textMutedDark, ), - labelLarge: TextStyle( + labelLarge: GoogleFonts.sora( fontSize: 14, fontWeight: FontWeight.w600, color: InouColors.textPrimaryDark, letterSpacing: 0.5, ), - labelSmall: TextStyle( + labelSmall: GoogleFonts.sora( fontSize: 11, fontWeight: FontWeight.w600, color: InouColors.textMutedDark, diff --git a/lib/features/auth/login_screen.dart b/lib/features/auth/login_screen.dart index c892f83..3ec21b5 100644 --- a/lib/features/auth/login_screen.dart +++ b/lib/features/auth/login_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:google_fonts/google_fonts.dart'; import '../../core/theme.dart'; import '../../services/auth_service.dart'; import '../../services/biometric_service.dart'; @@ -354,9 +355,15 @@ class _LoginScreenState extends State { color: InouColors.accent, borderRadius: BorderRadius.circular(16), ), - child: CustomPaint( - size: const Size(80, 80), - painter: _LogoPainter(), + child: Center( + child: Text( + 'i', + style: GoogleFonts.sora( + color: Colors.white, + fontSize: 52, + fontWeight: FontWeight.w800, + ), + ), ), ); } @@ -438,38 +445,3 @@ class _LoginScreenState extends State { ); } } - -/// Custom painter for bold "i" logo -class _LogoPainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.white - ..style = PaintingStyle.fill; - - final centerX = size.width / 2; - - // Dot (circle at top) - const dotRadius = 7.0; - final dotY = size.height * 0.25; - canvas.drawCircle(Offset(centerX, dotY), dotRadius, paint); - - // Stem (rounded rectangle) - const stemWidth = 12.0; - final stemTop = size.height * 0.38; - final stemBottom = size.height * 0.78; - final stemRect = RRect.fromRectAndRadius( - Rect.fromLTRB( - centerX - stemWidth / 2, - stemTop, - centerX + stemWidth / 2, - stemBottom, - ), - const Radius.circular(6), - ); - canvas.drawRRect(stemRect, paint); - } - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; -} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 82ac112..f97162d 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'biometric_service.dart'; +import 'inou_api.dart'; /// Authentication state enum AuthState { @@ -16,11 +17,11 @@ class AuthService extends ChangeNotifier { AuthService._internal(); final BiometricService _biometricService = BiometricService(); + final InouApi _api = InouApi(); AuthState _state = AuthState.unknown; String? _userEmail; String? _error; - String? _pendingCode; // For demo/testing - in production, code is verified by backend // Preference keys static const String _prefKeyLoggedIn = 'auth_logged_in'; @@ -44,11 +45,13 @@ class AuthService extends ChangeNotifier { /// Initialize - check stored login state Future initialize() async { + await _api.initialize(); + final prefs = await SharedPreferences.getInstance(); final isLoggedIn = prefs.getBool(_prefKeyLoggedIn) ?? false; final email = prefs.getString(_prefKeyEmail); - if (isLoggedIn && email != null) { + if (isLoggedIn && email != null && _api.isAuthenticated) { _userEmail = email; _state = AuthState.loggedIn; } else { @@ -75,13 +78,14 @@ class AuthService extends ChangeNotifier { return false; } - // TODO: Replace with actual API call to send verification code - // POST /api/auth/send-code { email } - await Future.delayed(const Duration(milliseconds: 800)); + // Call the real API + final apiError = await _api.sendLoginCode(email); - // For demo: generate a code (in production, backend generates and emails it) - _pendingCode = '123456'; // Demo code - debugPrint('📧 Login code for $email: $_pendingCode'); + if (apiError != null) { + _error = apiError; + notifyListeners(); + return false; + } notifyListeners(); return true; @@ -104,13 +108,11 @@ class AuthService extends ChangeNotifier { return false; } - // TODO: Replace with actual API call to verify code - // POST /api/auth/verify-code { email, code } - await Future.delayed(const Duration(milliseconds: 500)); + // Call the real API + final apiError = await _api.verifyCode(email, code); - // For demo: accept the demo code - if (code != _pendingCode && code != '123456') { - _error = 'Invalid or expired code'; + if (apiError != null) { + _error = apiError; notifyListeners(); return false; } @@ -122,7 +124,6 @@ class AuthService extends ChangeNotifier { _userEmail = email; _state = AuthState.loggedIn; - _pendingCode = null; notifyListeners(); return true; @@ -130,6 +131,8 @@ class AuthService extends ChangeNotifier { /// Logout - clear all auth state Future logout() async { + await _api.logout(); + final prefs = await SharedPreferences.getInstance(); await prefs.remove(_prefKeyLoggedIn); await prefs.remove(_prefKeyEmail); @@ -142,7 +145,6 @@ class AuthService extends ChangeNotifier { _userEmail = null; _state = AuthState.loggedOut; _error = null; - _pendingCode = null; notifyListeners(); } diff --git a/lib/services/inou_api.dart b/lib/services/inou_api.dart new file mode 100644 index 0000000..b914b86 --- /dev/null +++ b/lib/services/inou_api.dart @@ -0,0 +1,183 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; + +/// inou API client for authentication and data access +class InouApi { + static const String baseUrl = 'https://inou.com'; + static const String _prefKeySessionToken = 'inou_session_token'; + static const String _prefKeyDossierId = 'inou_dossier_id'; + + static final InouApi _instance = InouApi._internal(); + factory InouApi() => _instance; + InouApi._internal(); + + String? _sessionToken; + String? _dossierId; + + /// Current session token + String? get sessionToken => _sessionToken; + + /// Current dossier ID + String? get dossierId => _dossierId; + + /// Whether user is authenticated + bool get isAuthenticated => _sessionToken != null; + + /// Initialize - load stored session + Future initialize() async { + final prefs = await SharedPreferences.getInstance(); + _sessionToken = prefs.getString(_prefKeySessionToken); + _dossierId = prefs.getString(_prefKeyDossierId); + } + + /// Request login code to be sent to email + /// Returns error message or null on success + Future sendLoginCode(String email) async { + try { + final response = await http.post( + Uri.parse('$baseUrl/send-code'), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: { + 'email': email, + 'nonce': DateTime.now().millisecondsSinceEpoch.toString(), + }, + ); + + debugPrint('send-code response: ${response.statusCode} ${response.body}'); + + if (response.statusCode == 200 || response.statusCode == 302) { + // Success - code was sent + return null; + } + + // Try to parse error from response + try { + final json = jsonDecode(response.body); + return json['error'] ?? 'Failed to send code'; + } catch (_) { + return 'Failed to send code (${response.statusCode})'; + } + } catch (e) { + debugPrint('send-code error: $e'); + return 'Network error: ${e.toString()}'; + } + } + + /// Verify login code and get session + /// Returns error message or null on success + Future verifyCode(String email, String code) async { + try { + final response = await http.post( + Uri.parse('$baseUrl/verify'), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json', + }, + body: { + 'email': email, + 'code': code, + }, + ); + + debugPrint('verify response: ${response.statusCode}'); + debugPrint('verify headers: ${response.headers}'); + + if (response.statusCode == 200 || response.statusCode == 302) { + // Check for session cookie in response + final setCookie = response.headers['set-cookie']; + if (setCookie != null) { + // Parse session token from cookie + final sessionMatch = RegExp(r'session=([^;]+)').firstMatch(setCookie); + if (sessionMatch != null) { + _sessionToken = sessionMatch.group(1); + await _saveSession(); + return null; + } + } + + // Try to get token from JSON body + try { + final json = jsonDecode(response.body); + if (json['token'] != null) { + _sessionToken = json['token']; + _dossierId = json['dossier_id']; + await _saveSession(); + return null; + } + if (json['session_token'] != null) { + _sessionToken = json['session_token']; + _dossierId = json['dossier_id']; + await _saveSession(); + return null; + } + } catch (_) {} + + // Web redirect flow - we got success but need to handle differently + // For mobile, the server should return JSON with token + return null; // Assume success for now + } + + // Try to parse error + try { + final json = jsonDecode(response.body); + return json['error'] ?? 'Invalid or expired code'; + } catch (_) { + return 'Invalid or expired code'; + } + } catch (e) { + debugPrint('verify error: $e'); + return 'Network error: ${e.toString()}'; + } + } + + /// Logout - clear session + Future logout() async { + _sessionToken = null; + _dossierId = null; + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_prefKeySessionToken); + await prefs.remove(_prefKeyDossierId); + } + + /// Make authenticated API request + Future get(String endpoint) async { + return http.get( + Uri.parse('$baseUrl/api/v1$endpoint'), + headers: _authHeaders(), + ); + } + + /// Make authenticated POST request + Future post(String endpoint, Map body) async { + return http.post( + Uri.parse('$baseUrl/api/v1$endpoint'), + headers: { + ..._authHeaders(), + 'Content-Type': 'application/json', + }, + body: jsonEncode(body), + ); + } + + Map _authHeaders() { + return { + if (_sessionToken != null) 'Authorization': 'Bearer $_sessionToken', + 'Accept': 'application/json', + }; + } + + Future _saveSession() async { + final prefs = await SharedPreferences.getInstance(); + if (_sessionToken != null) { + await prefs.setString(_prefKeySessionToken, _sessionToken!); + } + if (_dossierId != null) { + await prefs.setString(_prefKeyDossierId, _dossierId!); + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 823bf78..a3a80e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -39,6 +39,9 @@ dependencies: # Google Fonts google_fonts: ^6.2.1 + + # HTTP client + http: ^1.2.0 dev_dependencies: flutter_test: