fix: v2.0.4+24 — login fixes, signup toggle, forgot-password, guest SnackBar, Google OAuth

- Google Sign-In: wire serverClientId (639347358523-mtkm...apps.googleusercontent.com) so idToken is returned on Android
- Email login: raise timeout 10s→25s, add single retry on SocketException/TimeoutException
- Forgot Password: real glassmorphism bottom sheet with safe-degrade SnackBar (endpoint missing on backend)
- Create Account: same-page AnimatedSwitcher toggle with glassmorphism signup form; delete old RegisterScreen
- Desktop parity: DesktopLoginScreen same-page toggle; delete DesktopRegisterScreen
- Guest mode: remove ScaffoldMessenger SnackBar from HomeScreen outer catch (inner _safe wrappers already return [])
- LoginScreen: clearSnackBars() on postFrameCallback to prevent carried-over SnackBars from prior screens
- ProGuard: add Google Sign-In + OkHttp keep rules
- Version bump: 2.0.0+20 → 2.0.4+24; settings _appVersion → 2.0.4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-19 21:40:17 +05:30
parent 5e00e431e3
commit ebe654f9c3
9 changed files with 879 additions and 368 deletions

View File

@@ -1,12 +1,15 @@
// lib/core/api/api_client.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io' show SocketException;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
import '../storage/token_storage.dart';
class ApiClient {
static const Duration _timeout = Duration(seconds: 10);
static const Duration _timeout = Duration(seconds: 25);
static const Duration _retryDelay = Duration(milliseconds: 600);
// Set to true to enable mock/offline development mode (useful when backend is unavailable)
static const bool _developmentMode = false;
@@ -28,13 +31,7 @@ class ApiClient {
late http.Response response;
try {
response = await http
.post(
Uri.parse(url),
headers: headers,
body: jsonEncode(finalBody),
)
.timeout(_timeout);
response = await _postWithRetry(url, headers, finalBody);
} catch (e) {
if (kDebugMode) debugPrint('ApiClient.post network error: $e');
@@ -100,6 +97,32 @@ class ApiClient {
return _handleResponse(url, response, finalBody);
}
/// POST with one retry on transient network errors.
/// Retries on SocketException / TimeoutException only.
Future<http.Response> _postWithRetry(
String url,
Map<String, String> headers,
Map<String, dynamic> body,
) async {
try {
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
} on SocketException {
if (kDebugMode) debugPrint('ApiClient.post retry after SocketException');
await Future.delayed(_retryDelay);
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
} on TimeoutException {
if (kDebugMode) debugPrint('ApiClient.post retry after TimeoutException');
await Future.delayed(_retryDelay);
return await http
.post(Uri.parse(url), headers: headers, body: jsonEncode(body))
.timeout(_timeout);
}
}
/// Upload a single file as multipart/form-data.
///
/// Returns the `file` object from the server response: