diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 1fafe09..41abf88 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -7,8 +7,8 @@ plugins { android { namespace = "com.example.autos" - compileSdk = flutter.compileSdkVersion - ndkVersion = flutter.ndkVersion + compileSdk = 36 + ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 @@ -20,25 +20,21 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId = "com.example.autos" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + targetSdk = 36 versionCode = flutter.versionCode versionName = flutter.versionName } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. signingConfig = signingConfigs.getByName("debug") } } } + flutter { source = "../.." } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index ac3b479..348c409 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart index ed7420e..d363eef 100644 --- a/lib/core/constants/api_endpoints.dart +++ b/lib/core/constants/api_endpoints.dart @@ -21,6 +21,9 @@ class ApiEndpoints { static const turn14Save = '/api/auth/turn14/save'; static const turn14Status = '/api/auth/turn14/status'; + ///Ebay + static const checkstorestatus = '/api/auth/ebay/store/checkstorestatus'; + ///Brands static const String brands = "/v1/brands"; diff --git a/lib/core/utils/date_time_utils.dart b/lib/core/utils/date_time_utils.dart new file mode 100644 index 0000000..b676ca1 --- /dev/null +++ b/lib/core/utils/date_time_utils.dart @@ -0,0 +1,26 @@ +String formatLastOpenedFromString(String? raw) { + if (raw == null || raw.isEmpty) return "—"; + + DateTime? date; + + try { + date = DateTime.parse(raw).toLocal(); + } catch (_) { + return "—"; + } + + final now = DateTime.now(); + final diff = now.difference(date); + + if (diff.inMinutes < 1) { + return "just now"; + } else if (diff.inMinutes < 60) { + return "${diff.inMinutes} mins ago"; + } else if (diff.inHours < 24) { + return "${diff.inHours} hrs ago"; + } else if (diff.inDays < 7) { + return "${diff.inDays} days ago"; + } else { + return "${date.day}/${date.month}/${date.year}"; + } +} diff --git a/lib/core/utils/ebay_webview_screen.dart b/lib/core/utils/ebay_webview_screen.dart new file mode 100644 index 0000000..6dfa4ee --- /dev/null +++ b/lib/core/utils/ebay_webview_screen.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class EbayWebViewScreen extends StatefulWidget { + final String url; + final String title; + + const EbayWebViewScreen({ + super.key, + required this.url, + required this.title, + }); + + @override + State createState() => _EbayWebViewScreenState(); +} + +class _EbayWebViewScreenState extends State { + late final WebViewController _controller; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + + _controller = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) { + setState(() => _isLoading = false); + }, + ), + ) + ..loadRequest(Uri.parse(widget.url)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Stack( + children: [ + WebViewWidget(controller: _controller), + + if (_isLoading) + const Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } +} diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/data/models/turn14_model.dart b/lib/data/models/turn14_model.dart index 9a2528e..093c47d 100644 --- a/lib/data/models/turn14_model.dart +++ b/lib/data/models/turn14_model.dart @@ -1,71 +1,13 @@ -import 'dart:convert'; import 'package:autos/domain/entities/turn14.dart'; - -class Turn14Response { - final String code; - final String message; - final String userId; - final String accessToken; - - const Turn14Response({ - required this.code, - required this.message, - required this.userId, - required this.accessToken, - }); - - factory Turn14Response.fromJson(Map json) { - return Turn14Response( - code: json['code'] ?? '', - message: json['message'] ?? '', - userId: json['userid'] ?? '', - accessToken: json['access_token'] ?? '', - ); - } - - Map toJson() { - return { - 'code': code, - 'message': message, - 'userid': userId, - 'access_token': accessToken, - }; - } - - /// Convert model → raw JSON string - String toRawJson() => jsonEncode(toJson()); - - /// Convert raw JSON string → model - factory Turn14Response.fromRawJson(String raw) => - Turn14Response.fromJson(jsonDecode(raw)); - - /// Convert Response → Domain Entity - Turn14Entity toEntity() { - return Turn14Entity( - code: code, - message: message, - userId: userId, - accessToken: accessToken, - ); - } - - @override - String toString() { - return 'Turn14Response(code: $code, message: $message, userId: $userId, accessToken: $accessToken)'; - } -} - - - class Turn14StatusModel { final String userId; final bool hasCredentials; final String? clientId; final String? clientSecret; final String? accessToken; - final int? expiresIn; // ✅ FIXED: should be int - final String? code; // ✅ ADDED - final String? message; // ✅ ADDED + final String? expiresIn; + final String? code; + final String? message; Turn14StatusModel({ required this.userId, @@ -78,7 +20,6 @@ class Turn14StatusModel { this.message, }); - /// ✅ SAFE FROM JSON factory Turn14StatusModel.fromJson(Map json) { final credentials = json["credentials"]; final tokenInfo = json["tokenInfo"]; @@ -86,21 +27,34 @@ class Turn14StatusModel { return Turn14StatusModel( userId: json["userid"]?.toString() ?? "", hasCredentials: json["hasCredentials"] ?? false, - clientId: credentials?["turn14clientid"], clientSecret: credentials?["turn14clientsecret"], - accessToken: tokenInfo?["access_token"], - expiresIn: tokenInfo?["expires_in"], - + expiresIn: tokenInfo?["expires_in"]?.toString(), code: json["code"], message: json["message"], ); } - /// ✅ MODEL → ENTITY (CLEAN ARCH) - Turn14StatusEntity toEntity() { - return Turn14StatusEntity( + Map toJson() => { + "userid": userId, + "hasCredentials": hasCredentials, + "credentials": { + "turn14clientid": clientId, + "turn14clientsecret": clientSecret, + }, + "tokenInfo": { + "access_token": accessToken, + "expires_in": expiresIn, + }, + "code": code, + "message": message, + }; + + Turn14Entity toEntity() { + return Turn14Entity( + code: code ?? '', + message: message ?? '', userId: userId, hasCredentials: hasCredentials, clientId: clientId, diff --git a/lib/data/repositories/ebay_repository_impl.dart b/lib/data/repositories/ebay_repository_impl.dart new file mode 100644 index 0000000..419c482 --- /dev/null +++ b/lib/data/repositories/ebay_repository_impl.dart @@ -0,0 +1,54 @@ +import 'dart:convert'; +import 'package:autos/core/constants/api_endpoints.dart'; +import 'package:autos/data/models/user_model.dart'; +import 'package:autos/data/sources/remote/api_service.dart'; +import 'package:autos/domain/entities/ebay.dart'; +import 'package:autos/data/repositories/token_repository.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +abstract class EbayRepository { + /// Return a single store or null if none exists + Future getEbayStatus(); +} + +class EbayRepositoryImpl implements EbayRepository { + final ApiService _apiService; + final TokenRepository _tokenRepository; + final FlutterSecureStorage _storage = const FlutterSecureStorage(); + static const _userKey = 'logged_in_user'; + + EbayRepositoryImpl(this._apiService, this._tokenRepository); + + @override + Future getEbayStatus() async { + // 1️⃣ Read logged-in user + final jsonString = await _storage.read(key: _userKey); + if (jsonString == null) return null; + + final user = UserModel.fromJson(jsonDecode(jsonString)); + + // 2️⃣ Get valid token + final token = await _tokenRepository.getValidToken(user.id); + + // 3️⃣ Call eBay API + final response = await _apiService.postWithOptions( + ApiEndpoints.checkstorestatus, + data: {"userid": user.id}, + headers: {"Authorization": "Bearer $token"}, + ); + + debugPrint('🟢 Ebay API Raw Response: ${response.data}'); + + // 4️⃣ Extract 'store' from response + final data = response.data; + if (data == null || data is! Map) return null; + + final storeJson = data['store']; + if (storeJson == null || storeJson is! Map) return null; + + // 5️⃣ Convert to entity + final storeEntity = EbayEntity.fromJson(storeJson); + return storeEntity; + } +} diff --git a/lib/data/repositories/token_repository.dart b/lib/data/repositories/token_repository.dart index 19acc72..909f4c0 100644 --- a/lib/data/repositories/token_repository.dart +++ b/lib/data/repositories/token_repository.dart @@ -32,26 +32,26 @@ class TokenRepository { /// 🔄 Refresh Token API Future _refreshToken(String userId) async { - final response = await apiService.post( - ApiEndpoints.getToken, - {"userid": userId}, - ); + final response = await apiService.post(ApiEndpoints.getToken, { + "userid": userId, + }); final data = response.data; - - if (data["code"] != "TOKEN_UPDATED") { - throw Exception(data["message"]); - } - + final code = data["code"]; final token = data["access_token"]; - /// ✅ Save New Token + Timestamp - await _storage.write(key: _tokenKey, value: token); - await _storage.write( - key: _tokenTimeKey, - value: DateTime.now().toIso8601String(), - ); - - return token; + if (code == "TOKEN_VALID" || code == "TOKEN_UPDATED") { + // ✅ Save token and timestamp if updated, otherwise just use existing token + if (code == "TOKEN_UPDATED") { + await _storage.write(key: _tokenKey, value: token); + await _storage.write( + key: _tokenTimeKey, + value: DateTime.now().toIso8601String(), + ); + } + return token; + } else { + throw Exception("Failed to get Turn14 token: ${data["message"]}"); + } } } diff --git a/lib/data/repositories/turn14_repository_impl.dart b/lib/data/repositories/turn14_repository_impl.dart index ee5d645..10389fa 100644 --- a/lib/data/repositories/turn14_repository_impl.dart +++ b/lib/data/repositories/turn14_repository_impl.dart @@ -37,7 +37,7 @@ class Turn14RepositoryImpl implements Turn14Repository { if (data['code'] == "TURN14_SAVED") { /// Convert Response → Entity - return Turn14Response.fromJson(data).toEntity(); + return Turn14StatusModel.fromJson(data).toEntity(); } else { throw Exception( data['message'] ?? "Failed to save Turn14 credentials", @@ -53,7 +53,7 @@ class Turn14RepositoryImpl implements Turn14Repository { } } - Future status(String userId) async { + Future status(String userId) async { try { final response = await _apiService.post(ApiEndpoints.turn14Status, { "userid": userId, diff --git a/lib/domain/entities/ebay.dart b/lib/domain/entities/ebay.dart new file mode 100644 index 0000000..52e18ec --- /dev/null +++ b/lib/domain/entities/ebay.dart @@ -0,0 +1,45 @@ +class EbayEntity { + final String userId; + final String storeName; + final String storeDescription; + final String storeUrl; + final String storeUrlPath; + final String? storeLastOpenedTimeRaw; + final String storeLogoUrl; + + EbayEntity({ + required this.userId, + required this.storeName, + required this.storeDescription, + required this.storeUrl, + required this.storeUrlPath, + required this.storeLastOpenedTimeRaw, + required this.storeLogoUrl, + }); + + /// Create an instance from JSON map + factory EbayEntity.fromJson(Map json) { + return EbayEntity( + userId: json['userid'] as String, + storeName: json['store_name'] as String, + storeDescription: json['store_description'] as String, + storeUrl: json['store_url'] as String, + storeUrlPath: json['store_url_path'] as String, + storeLastOpenedTimeRaw: json['store_last_opened_time_raw'] as String?, + storeLogoUrl: json['store_logo_url'] as String, + ); + } + + /// Convert instance to JSON map + Map toJson() { + return { + 'userid': userId, + 'store_name': storeName, + 'store_description': storeDescription, + 'store_url': storeUrl, + 'store_url_path': storeUrlPath, + 'store_last_opened_time_raw': storeLastOpenedTimeRaw, + 'store_logo_url': storeLogoUrl, + }; + } +} diff --git a/lib/domain/entities/turn14.dart b/lib/domain/entities/turn14.dart index af0c3a8..02837db 100644 --- a/lib/domain/entities/turn14.dart +++ b/lib/domain/entities/turn14.dart @@ -2,16 +2,32 @@ class Turn14Entity { final String code; final String message; final String userId; - final String accessToken; + + /// Credentials + final bool hasCredentials; + final String? clientId; + final String? clientSecret; + + /// Token Info + final String? accessToken; + final String? expiresIn; const Turn14Entity({ required this.code, required this.message, required this.userId, - required this.accessToken, + required this.hasCredentials, + this.clientId, + this.clientSecret, + this.accessToken, + this.expiresIn, }); + + bool get isConnected => + hasCredentials == true && accessToken != null && accessToken!.isNotEmpty; } + class Turn14StatusEntity { final String userId; final bool hasCredentials; diff --git a/lib/main.dart b/lib/main.dart index 0fa2604..7dee896 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,8 +6,6 @@ import 'package:autos/presentation/screens/auth/login_screen.dart'; import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; - -// ✅ ADD THIS IMPORT import 'package:onesignal_flutter/onesignal_flutter.dart'; void main() { @@ -18,7 +16,6 @@ void main() { OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085"); OneSignal.Notifications.requestPermission(false); - // KEEP YOUR ORIGINAL LOGIC runApp(const ProviderScope(child: MyApp())); } diff --git a/lib/presentation/providers/ebay_provider.dart b/lib/presentation/providers/ebay_provider.dart new file mode 100644 index 0000000..a0abda5 --- /dev/null +++ b/lib/presentation/providers/ebay_provider.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import 'package:autos/data/models/turn14_model.dart'; +import 'package:autos/data/repositories/ebay_repository_impl.dart'; +import 'package:autos/data/repositories/token_repository.dart'; +import 'package:autos/data/sources/remote/api_service.dart'; +import 'package:autos/domain/entities/ebay.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/legacy.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// ✅ API SERVICE PROVIDER +final ebayApiServiceProvider = Provider((ref) => ApiService()); + +/// ✅ TOKEN REPOSITORY PROVIDER +final _tokenRepositoryProvider = Provider((ref) { + final api = ref.read(ebayApiServiceProvider); + return TokenRepository(api); +}); + +/// ✅ REPOSITORY PROVIDER +final ebayRepositoryProvider = Provider((ref) { + final api = ref.read(ebayApiServiceProvider); + final tokenRepo = ref.read(_tokenRepositoryProvider); + return EbayRepositoryImpl(api, tokenRepo); +}); + +/// ✅ STATE NOTIFIER PROVIDER +final ebayProvider = + StateNotifierProvider>((ref) { + final repo = ref.read(ebayRepositoryProvider); + return EbayNotifier(repo); +}); + +/// ✅ NOTIFIER +class EbayNotifier extends StateNotifier> { + final EbayRepositoryImpl repository; + + EbayNotifier(this.repository) : super(const AsyncValue.loading()) { + fetchStore(); + } + + /// ✅ FETCH STORE + Future fetchStore() async { + try { + state = const AsyncValue.loading(); + + // 1️⃣ Read stored credentials + const storage = FlutterSecureStorage(); + final saved = await storage.read(key: "turn14_credentials"); + if (saved == null) { + state = const AsyncValue.data(null); + return; + } + + final decoded = jsonDecode(saved); + final turn14 = Turn14StatusModel.fromJson(decoded); + if (turn14.hasCredentials != true) { + state = const AsyncValue.data(null); + return; + } + + // 2️⃣ Fetch store from backend + final EbayEntity? store = await repository.getEbayStatus(); + // repository should now return a single EbayEntity? instead of List + + // 3️⃣ Update state + state = AsyncValue.data(store); + } catch (e, st) { + debugPrint("❌ EBAY FETCH ERROR: $e"); + state = AsyncValue.error(e, st); + } + } + +} diff --git a/lib/presentation/providers/turn14_provider.dart b/lib/presentation/providers/turn14_provider.dart index 3c3e4e7..92f5d47 100644 --- a/lib/presentation/providers/turn14_provider.dart +++ b/lib/presentation/providers/turn14_provider.dart @@ -8,18 +8,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - /// ------------------------------------------------------------ /// Service Providers /// ------------------------------------------------------------ -final turn14ApiServiceProvider = - Provider((ref) => ApiService()); - -final turn14RepositoryProvider = Provider((ref) { - return Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider)); -}); +final turn14ApiServiceProvider = Provider( + (ref) => ApiService(), +); +final turn14RepositoryProvider = Provider( + (ref) => Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider)), +); /// ------------------------------------------------------------ /// Turn14 Notifier @@ -27,14 +26,15 @@ final turn14RepositoryProvider = Provider((ref) { class Turn14Notifier extends StateNotifier> { final Turn14RepositoryImpl repository; - final _storage = const FlutterSecureStorage(); + final FlutterSecureStorage _storage = const FlutterSecureStorage(); - static const _turn14StorageKey = "turn14_credentials"; + static const String _turn14StorageKey = "turn14_credentials"; Turn14Notifier(this.repository) : super(const AsyncValue.data(null)); - - /// Save Turn14 credentials + // ------------------------------------------------------------ + // Save Turn14 credentials (API + local) + // ------------------------------------------------------------ Future saveCredentials({ required String userId, required String clientId, @@ -43,50 +43,127 @@ class Turn14Notifier extends StateNotifier> { state = const AsyncValue.loading(); try { - final response = await repository.save( + final Turn14Entity entity = await repository.save( userId: userId, clientId: clientId, clientSecret: clientSecret, ); - // if (response is Turn14Response) { - // await _storage.write( - // key: _turn14StorageKey, - // value: response.toRawJson(), - // ); - // } + // Convert entity → model (for persistence) + final model = Turn14StatusModel( + userId: entity.userId, + hasCredentials: entity.hasCredentials, + clientId: entity.clientId, + clientSecret: entity.clientSecret, + accessToken: entity.accessToken, + expiresIn: entity.expiresIn, + code: entity.code, + message: entity.message, + ); - state = AsyncValue.data(response); + await _storage.write( + key: _turn14StorageKey, + value: jsonEncode(model.toJson()), + ); + + state = AsyncValue.data(entity); } catch (e, st) { state = AsyncValue.error(e, st); } } - /// Load saved Turn14 credentials + // ------------------------------------------------------------ + // Load Turn14 from LOCAL storage + // ------------------------------------------------------------ Future loadSavedCredentials() async { - final saved = await _storage.read(key: _turn14StorageKey); - if (saved == null) return; + try { + final saved = await _storage.read(key: _turn14StorageKey); + if (saved == null) { + state = const AsyncValue.data(null); + return; + } - final decoded = jsonDecode(saved); - final model = Turn14Response.fromJson(decoded); + final decoded = jsonDecode(saved); + final model = Turn14StatusModel.fromJson(decoded); - state = AsyncValue.data(model as Turn14Entity?); + final entity = Turn14Entity( + code: model.code ?? '', + message: model.message ?? '', + userId: model.userId, + hasCredentials: model.hasCredentials, + clientId: model.clientId, + clientSecret: model.clientSecret, + accessToken: model.accessToken, + expiresIn: model.expiresIn, + ); + + state = AsyncValue.data(entity); + } catch (e, st) { + state = AsyncValue.error(e, st); + } } - /// Clear saved Turn14 data + // ------------------------------------------------------------ + // Load Turn14 status from API + persist locally + // ------------------------------------------------------------ + Future loadTurn14Status(String userId) async { + state = const AsyncValue.loading(); + + try { + final status = await repository.status(userId); + // Turn14StatusEntity + + if (status.hasCredentials == true) { + final entity = Turn14Entity( + code: '', + message: '', + userId: status.userId, + hasCredentials: status.hasCredentials, + clientId: status.clientId, + clientSecret: status.clientSecret, + accessToken: status.accessToken, + expiresIn: status.expiresIn?.toString(), + ); + + final model = Turn14StatusModel( + userId: entity.userId, + hasCredentials: entity.hasCredentials, + clientId: entity.clientId, + clientSecret: entity.clientSecret, + accessToken: entity.accessToken, + expiresIn: entity.expiresIn, + code: entity.code, + message: entity.message, + ); + + await _storage.write( + key: _turn14StorageKey, + value: jsonEncode(model.toJson()), + ); + + state = AsyncValue.data(entity); + } else { + await clear(); + } + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } + + // ------------------------------------------------------------ + // Clear Turn14 data (logout / user switch) + // ------------------------------------------------------------ Future clear() async { await _storage.delete(key: _turn14StorageKey); state = const AsyncValue.data(null); } } - /// ------------------------------------------------------------ /// Riverpod Provider /// ------------------------------------------------------------ final turn14Provider = - StateNotifierProvider>((ref) { - final repository = ref.read(turn14RepositoryProvider); - return Turn14Notifier(repository); -}); + StateNotifierProvider>( + (ref) => Turn14Notifier(ref.read(turn14RepositoryProvider)), +); diff --git a/lib/presentation/providers/user_provider.dart b/lib/presentation/providers/user_provider.dart index 108347b..b6bdc27 100644 --- a/lib/presentation/providers/user_provider.dart +++ b/lib/presentation/providers/user_provider.dart @@ -17,8 +17,9 @@ final userRepositoryProvider = Provider( ); /// SINGLE GLOBAL USER PROVIDER -final userProvider = - StateNotifierProvider>((ref) { +final userProvider = StateNotifierProvider>(( + ref, +) { final repo = ref.read(userRepositoryProvider); return UserNotifier(ref, repo); }); @@ -34,8 +35,7 @@ class UserNotifier extends StateNotifier> { AuthAction lastAction = AuthAction.idle; - UserNotifier(this.ref, this.repository) - : super(const AsyncValue.data(null)); + UserNotifier(this.ref, this.repository) : super(const AsyncValue.data(null)); // AUTO LOGIN ON APP START Future loadUserFromStorage() async { @@ -65,51 +65,51 @@ class UserNotifier extends StateNotifier> { // 2️⃣ Fetch FULL user details final fullUser = await repository.getUserDetails(partialUser.id); - // 3️⃣ Store FULL user in secure storage + // 3️⃣ Store FULL user in secure storage await _storage.write(key: _userKey, value: fullUser.toRawJson()); - // 4️⃣ Update provider ONCE with FULL data + // 4️⃣ Update provider ONCE with FULL data state = AsyncValue.data(fullUser); } catch (e, st) { state = AsyncValue.error(e, st); } } - // SIGNUP - Future signup( - String name, - String email, - String password, - String phone, -) async { - lastAction = AuthAction.signup; + // SIGNUP + Future signup( + String name, + String email, + String password, + String phone, + ) async { + lastAction = AuthAction.signup; - try { - state = const AsyncValue.loading(); + try { + state = const AsyncValue.loading(); - // ✅ 1️⃣ Signup → returns USER ENTITY - final user = await repository.signup(name, email, password, phone); + // ✅ 1️⃣ Signup → returns USER ENTITY + final user = await repository.signup(name, email, password, phone); - // ✅ 2️⃣ Convert User → UserModel for storage - final userModel = UserModel.fromEntity(user); + // ✅ 2️⃣ Convert User → UserModel for storage + final userModel = UserModel.fromEntity(user); - // ✅ 3️⃣ Store safely - await _storage.write( - key: _userKey, - value: userModel.toRawJson(), - ); + // ✅ 3️⃣ Store safely + await _storage.write(key: _userKey, value: userModel.toRawJson()); - // ✅ 4️⃣ Update provider state - state = AsyncValue.data(user); - } catch (e, st) { - state = AsyncValue.error(e, st); + // ✅ 4️⃣ Update provider state + state = AsyncValue.data(user); + } catch (e, st) { + state = AsyncValue.error(e, st); + } } -} - // ✅ ✅ LOGOUT Future logout() async { - await _storage.delete(key: _userKey); + // await _storage.delete(key: _userKey); + // await _storage.delete(key: "turn14_credentials"); + // state = const AsyncValue.data(null); + + await _storage.deleteAll(); state = const AsyncValue.data(null); } diff --git a/lib/presentation/screens/ebay/ebay_screen.dart b/lib/presentation/screens/ebay/ebay_screen.dart index 467902c..7c94475 100644 --- a/lib/presentation/screens/ebay/ebay_screen.dart +++ b/lib/presentation/screens/ebay/ebay_screen.dart @@ -1,33 +1,42 @@ +import 'package:autos/core/routing/route_paths.dart'; import 'package:autos/core/theme/app_typography.dart'; +import 'package:autos/core/utils/date_time_utils.dart'; +import 'package:autos/core/utils/ebay_webview_screen.dart'; +import 'package:autos/domain/entities/ebay.dart'; +import 'package:autos/presentation/providers/ebay_provider.dart'; import 'package:flutter/material.dart'; import 'package:autos/core/widgets/hamburger_button.dart'; import 'package:autos/core/widgets/side_menu.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class EbayScreen extends StatefulWidget { +class EbayScreen extends ConsumerStatefulWidget { const EbayScreen({super.key}); @override - State createState() => _EbayScreenState(); + ConsumerState createState() => _EbayScreenState(); } -class _EbayScreenState extends State { - final GlobalKey _scaffoldKey = GlobalKey(); - String selected = "ebay"; +class _EbayScreenState extends ConsumerState { + final GlobalKey scaffoldKey = GlobalKey(); + + @override + void initState() { + super.initState(); + + /// ✅ Fetch ONCE + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(ebayProvider.notifier).fetchStore(); + }); + } @override Widget build(BuildContext context) { - final double topPadding = MediaQuery.of(context).padding.top + 16; + final storeAsync = ref.watch(ebayProvider); + final topPadding = MediaQuery.of(context).padding.top + 16; return Scaffold( - key: _scaffoldKey, - drawer: SideMenu( - selected: selected, - onItemSelected: (key) { - setState(() => selected = key); - }, - ), - - // backgroundColor: const Color(0xFFEFFAFF), + key: scaffoldKey, + drawer: SideMenu(selected: "ebay", onItemSelected: (_) {}), body: Stack( children: [ /// TITLE @@ -43,80 +52,312 @@ class _EbayScreenState extends State { ), ), - /// MAIN BOX UI + /// CONTENT + Positioned.fill( + top: topPadding + 60, + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 720), + child: storeAsync.when( + loading: () => + const Center(child: CircularProgressIndicator()), + error: (e, _) => _errorCard(e.toString()), + data: (store) => store == null + ? _connectCard(context) + : _connectedCard(context, store), + ), + ), + ), + ); + }, + ), + ), + + HamburgerButton(scaffoldKey: scaffoldKey), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // CONNECT (NOT CONNECTED) + // --------------------------------------------------------------------------- + Widget _connectCard(BuildContext context) { + return _cardWrapper( + Column( + children: [ + const Text( + "Connect your eBay store to enable product sync, inventory updates, and order flow.", + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15, color: Colors.black54, height: 1.5), + ), + const SizedBox(height: 30), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00CFFF), + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text( + "Connect your eBay store", + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // CONNECTED UI (RESPONSIVE) + // --------------------------------------------------------------------------- + Widget _connectedCard(BuildContext context, EbayEntity store) { + final isSmall = MediaQuery.of(context).size.width < 400; + + return _cardWrapper( + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// CONNECTED BADGE Center( child: Container( - width: MediaQuery.of(context).size.width * 0.90, - padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40), - margin: EdgeInsets.only(top: topPadding + 60), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6), decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(25), - boxShadow: [ - BoxShadow( - color: Colors.black12, - blurRadius: 25, - offset: const Offset(0, 12), - ), - ], + color: Colors.green.shade50, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.green.shade300), ), - child: Column( + child: const Row( mainAxisSize: MainAxisSize.min, children: [ - /// Description + Icon(Icons.check_circle, color: Colors.green, size: 18), + SizedBox(width: 6), Text( - "Connect your eBay store to enable product sync, inventory updates, and order flow.", - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 15, - color: Colors.black54, - height: 1.5, + "Connected", + style: TextStyle( + color: Colors.green, + fontWeight: FontWeight.w600, ), ), - - const SizedBox(height: 30), - - /// BUTTON - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - // TODO: Add eBay authorization flow - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF00CFFF), - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: const Text( - "Connect your eBay store", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w600, - fontSize: 16, - ), - ), - ), - ), - - const SizedBox(height: 20), - - Text( - "You'll be redirected to eBay to authorize access, then returned here.", - style: const TextStyle(fontSize: 13, color: Colors.black45), - textAlign: TextAlign.center, - ), ], ), ), ), - /// HAMBURGER BUTTON - HamburgerButton(scaffoldKey: _scaffoldKey), + const SizedBox(height: 16), + + /// TITLE + Center( + child: Text( + "eBay connected successfully 🎉", + style: AppTypo.h3.copyWith( + fontWeight: FontWeight.w700, + color: Color(0xFF00BFFF), + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 24), + + /// STORE CARD + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.grey.shade200), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// HEADER + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + store.storeLogoUrl.isNotEmpty + ? Image.network( + store.storeLogoUrl, + height: 60, + width: 60, + ) + : const Icon(Icons.store, size: 60), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + store.storeName, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + "Last opened: ${formatLastOpenedFromString(store.storeLastOpenedTimeRaw)}", + style: const TextStyle(color: Colors.black54), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 16), + + /// DESCRIPTION + Text( + store.storeDescription, + textAlign: TextAlign.justify, + style: const TextStyle(fontSize: 14, height: 1.5), + ), + + const SizedBox(height: 20), + + /// ACTION BUTTONS (RESPONSIVE) + isSmall + ? Column( + children: [ + _primaryButton("Visit eBay Store", () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => EbayWebViewScreen( + url: store.storeUrl, + title: store.storeName, + ), + ), + ); + }), + + const SizedBox(height: 12), + _outlineButton("Go to Dashboard", () { + Navigator.pushNamed( + context, + AppRoutePaths.dashboard, + ); + }), + ], + ) + : Row( + children: [ + _primaryButton("Visit eBay Store", () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => EbayWebViewScreen( + url: store.storeUrl, + title: store.storeName, + ), + ), + ); + }), + + const SizedBox(width: 12), + _outlineButton("Go to Dashboard", () { + Navigator.pushNamed( + context, + AppRoutePaths.dashboard, + ); + }), + ], + ), + + const SizedBox(height: 20), + + /// CONNECT ANOTHER + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () {}, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00CFFF), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text("Connect another eBay store"), + ), + ), + + const SizedBox(height: 8), + const Text( + "Use this to link an additional eBay store.", + style: TextStyle(color: Colors.black54, fontSize: 13), + ), + ], + ), + ), ], ), ); } + + // --------------------------------------------------------------------------- + Widget _primaryButton(String text, VoidCallback onTap) { + return ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF00CFFF), + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text(text), + ); + } + + Widget _outlineButton(String text, VoidCallback onTap) { + return OutlinedButton( + onPressed: onTap, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text(text), + ); + } + + Widget _errorCard(String message) { + return Center( + child: Text( + message, + style: const TextStyle(color: Colors.red), + textAlign: TextAlign.center, + ), + ); + } + + Widget _cardWrapper(Widget child) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(24), + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 25, + offset: Offset(0, 12), + ), + ], + ), + child: child, + ); + } } diff --git a/lib/presentation/screens/turn14_screen/turn14_screen.dart b/lib/presentation/screens/turn14_screen/turn14_screen.dart index 5a6a7d5..6f52fe9 100644 --- a/lib/presentation/screens/turn14_screen/turn14_screen.dart +++ b/lib/presentation/screens/turn14_screen/turn14_screen.dart @@ -18,18 +18,56 @@ class _Turn14ScreenState extends ConsumerState { final GlobalKey _scaffoldKey = GlobalKey(); String selected = 'turn14'; - // controllers final TextEditingController clientIdController = TextEditingController(); final TextEditingController clientSecretController = TextEditingController(); + bool _obscure = true; + bool _autoFilledOnce = false; + + @override + void initState() { + super.initState(); + + Future.microtask(() { + final user = ref.read(userProvider).value; + if (user != null) { + ref.read(turn14Provider.notifier).loadTurn14Status(user.id); + } + }); + } + @override Widget build(BuildContext context) { final asyncUser = ref.watch(userProvider); final turn14State = ref.watch(turn14Provider); - final user = asyncUser.value; final double topPadding = MediaQuery.of(context).padding.top + 16; + /// ✅ SAFE AUTO-FILL (only once) + turn14State.when( + data: (data) { + final connected = data?.isConnected == true; + + if (connected) { + clientIdController.text = data?.clientId ?? ''; + clientSecretController.text = data?.clientSecret ?? ''; + } else { + clientIdController.clear(); + clientSecretController.clear(); + } + }, + loading: () {}, + error: (_, __) { + clientIdController.clear(); + clientSecretController.clear(); + }, + ); + + final bool isConnected = turn14State.maybeWhen( + data: (data) => data?.isConnected == true, + orElse: () => false, + ); + return Scaffold( key: _scaffoldKey, drawer: SideMenu( @@ -44,27 +82,41 @@ class _Turn14ScreenState extends ConsumerState { top: topPadding, left: 0, right: 0, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, + child: Column( children: [ Text( "Turn14 Settings", style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700), ), + const SizedBox(height: 6), + + // /// ✅ STATUS BADGE + // if (turn14State.isLoading) + // const CircularProgressIndicator(strokeWidth: 2) + // else if (isConnected) + // const Text( + // "✅ Connected", + // style: TextStyle(color: Colors.green), + // ) + // else + // const Text( + // "⚠️ Not Connected", + // style: TextStyle(color: Colors.orange), + // ), ], ), ), - /// Main Scrollable UI + /// MAIN UI SingleChildScrollView( physics: const BouncingScrollPhysics(), - padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20), + padding: EdgeInsets.fromLTRB(16, topPadding + 80, 16, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Text("⚡", style: const TextStyle(fontSize: 28)), + const Text("⚡", style: TextStyle(fontSize: 28)), const SizedBox(width: 8), Expanded( child: Text( @@ -82,13 +134,10 @@ class _Turn14ScreenState extends ConsumerState { _inputField(label: "Client ID", controller: clientIdController), const SizedBox(height: 20), - _passwordField( - label: "Secret Key", - controller: clientSecretController, - ), + _passwordField(label: "Secret Key"), const SizedBox(height: 25), - /// SAVE BUTTON + /// ✅ SAVE BUTTON SizedBox( width: double.infinity, child: ElevatedButton( @@ -99,68 +148,59 @@ class _Turn14ScreenState extends ConsumerState { ), backgroundColor: const Color(0xFF00C9FF), ), - onPressed: turn14State.isLoading - ? null - : () async { - if (user == null) { - Fluttertoast.showToast( - msg: "⚠️ User not found", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.red, - textColor: Colors.white, - fontSize: 16.0, - ); - return; - } + onPressed: () async { + if (turn14State.isLoading) { + return; // prevent double click + } - if (clientIdController.text.trim().isEmpty || - clientSecretController.text.trim().isEmpty) { - Fluttertoast.showToast( - msg: "⚠️ Please fill all fields", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.orange, - textColor: Colors.white, - fontSize: 16.0, - ); - return; - } + if (isConnected) { + Fluttertoast.showToast(msg: "✅ Already connected"); + return; + } - await ref - .read(turn14Provider.notifier) - .saveCredentials( - userId: user.id, - clientId: clientIdController.text.trim(), - clientSecret: - clientSecretController.text.trim(), - ); + if (user == null) { + Fluttertoast.showToast(msg: "⚠️ User not found"); + return; + } - Fluttertoast.showToast( - msg: "✅ Turn14 Credentials Saved Successfully", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.BOTTOM, - backgroundColor: Colors.green, - textColor: Colors.white, - fontSize: 16.0, - ); - }, + if (clientIdController.text.trim().isEmpty || + clientSecretController.text.trim().isEmpty) { + Fluttertoast.showToast( + msg: "⚠️ Please fill all fields", + ); + return; + } + + await ref + .read(turn14Provider.notifier) + .saveCredentials( + userId: user.id, + clientId: clientIdController.text.trim(), + clientSecret: clientSecretController.text.trim(), + ); + + Fluttertoast.showToast( + msg: "✅ Turn14 Connected Successfully", + ); + }, child: turn14State.isLoading - ? const CircularProgressIndicator(color: Colors.white) + ? const SizedBox( + height: 22, + width: 22, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) : const Text( "Save Credentials", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 16, - ), + style: TextStyle(fontWeight: FontWeight.bold), ), ), ), const SizedBox(height: 20), - - _infoBox(), + _infoBox(isConnected), const SizedBox(height: 20), _tipsBox(), ], @@ -173,7 +213,8 @@ class _Turn14ScreenState extends ConsumerState { ); } - /// UI COMPONENTS + /// ---------------- UI COMPONENTS ---------------- + Widget _inputField({ required String label, required TextEditingController controller, @@ -198,58 +239,49 @@ class _Turn14ScreenState extends ConsumerState { ); } - Widget _passwordField({ - required String label, - required TextEditingController controller, - }) { - bool _obscure = true; - - return StatefulBuilder( - builder: (context, setStateSB) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: const TextStyle(fontWeight: FontWeight.w600)), - const SizedBox(height: 8), - TextField( - controller: controller, - obscureText: _obscure, - decoration: InputDecoration( - filled: true, - fillColor: const Color(0xFFF0F6FF), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide.none, - ), - suffixIcon: IconButton( - icon: Icon( - _obscure ? Icons.visibility_off : Icons.visibility, - ), - onPressed: () => setStateSB(() => _obscure = !_obscure), - ), - ), + Widget _passwordField({required String label}) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 8), + TextField( + controller: clientSecretController, + obscureText: _obscure, + decoration: InputDecoration( + filled: true, + fillColor: const Color(0xFFF0F6FF), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, ), - ], - ); - }, + suffixIcon: IconButton( + icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility), + onPressed: () => setState(() => _obscure = !_obscure), + ), + ), + ), + ], ); } - Widget _infoBox() { + Widget _infoBox(bool isConnected) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color(0xFFE8F1FF), + color: isConnected ? Colors.green.shade50 : const Color(0xFFE8F1FF), borderRadius: BorderRadius.circular(10), ), child: Row( - children: const [ - Icon(Icons.info, color: Colors.blue), - SizedBox(width: 10), + children: [ + Icon(Icons.info, color: isConnected ? Colors.green : Colors.blue), + const SizedBox(width: 10), Text( - "No credentials saved yet.", - style: TextStyle(color: Colors.black87), + isConnected + ? "Existing Turn14 credentials loaded." + : "No credentials saved yet.", + style: const TextStyle(color: Colors.black87), ), ], ), @@ -264,9 +296,9 @@ class _Turn14ScreenState extends ConsumerState { color: const Color(0xFFE8FCFF), borderRadius: BorderRadius.circular(10), ), - child: Column( + child: const Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text( "💡 Connection Tips", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5a2c27b..81243db 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,9 +8,11 @@ import Foundation import flutter_inappwebview_macos import flutter_secure_storage_macos import path_provider_foundation +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 2797eac..4079f66 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -789,6 +789,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + webview_flutter: + dependency: "direct main" + description: + name: webview_flutter + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + url: "https://pub.dev" + source: hosted + version: "4.13.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "9a25f6b4313978ba1c2cda03a242eea17848174912cfb4d2d8ee84a556f248e3" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + url: "https://pub.dev" + source: hosted + version: "2.14.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f + url: "https://pub.dev" + source: hosted + version: "3.23.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 57096ea..445cc44 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: youtube_player_flutter: ^9.1.3 flutter_launcher_icons: ^0.14.4 onesignal_flutter: ^5.3.4 + webview_flutter: ^4.13.0 dev_dependencies: flutter_test: