ebay screen connected to backend.

This commit is contained in:
bala 2025-12-20 23:21:32 +05:30
parent c6f2b15453
commit a7d57dfa72
21 changed files with 952 additions and 344 deletions

View File

@ -7,8 +7,8 @@ plugins {
android { android {
namespace = "com.example.autos" namespace = "com.example.autos"
compileSdk = flutter.compileSdkVersion compileSdk = 36
ndkVersion = flutter.ndkVersion ndkVersion = "27.0.12077973"
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@ -20,25 +20,21 @@ android {
} }
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.autos" 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 minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion targetSdk = 36
versionCode = flutter.versionCode versionCode = flutter.versionCode
versionName = flutter.versionName versionName = flutter.versionName
} }
buildTypes { buildTypes {
release { 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") signingConfig = signingConfigs.getByName("debug")
} }
} }
} }
flutter { flutter {
source = "../.." source = "../.."
} }

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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

View File

@ -21,6 +21,9 @@ class ApiEndpoints {
static const turn14Save = '/api/auth/turn14/save'; static const turn14Save = '/api/auth/turn14/save';
static const turn14Status = '/api/auth/turn14/status'; static const turn14Status = '/api/auth/turn14/status';
///Ebay
static const checkstorestatus = '/api/auth/ebay/store/checkstorestatus';
///Brands ///Brands
static const String brands = "/v1/brands"; static const String brands = "/v1/brands";

View File

@ -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}";
}
}

View File

@ -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<EbayWebViewScreen> createState() => _EbayWebViewScreenState();
}
class _EbayWebViewScreenState extends State<EbayWebViewScreen> {
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(),
),
],
),
);
}
}

View File

@ -1,71 +1,13 @@
import 'dart:convert';
import 'package:autos/domain/entities/turn14.dart'; 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<String, dynamic> json) {
return Turn14Response(
code: json['code'] ?? '',
message: json['message'] ?? '',
userId: json['userid'] ?? '',
accessToken: json['access_token'] ?? '',
);
}
Map<String, dynamic> 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 { class Turn14StatusModel {
final String userId; final String userId;
final bool hasCredentials; final bool hasCredentials;
final String? clientId; final String? clientId;
final String? clientSecret; final String? clientSecret;
final String? accessToken; final String? accessToken;
final int? expiresIn; // FIXED: should be int final String? expiresIn;
final String? code; // ADDED final String? code;
final String? message; // ADDED final String? message;
Turn14StatusModel({ Turn14StatusModel({
required this.userId, required this.userId,
@ -78,7 +20,6 @@ class Turn14StatusModel {
this.message, this.message,
}); });
/// SAFE FROM JSON
factory Turn14StatusModel.fromJson(Map<String, dynamic> json) { factory Turn14StatusModel.fromJson(Map<String, dynamic> json) {
final credentials = json["credentials"]; final credentials = json["credentials"];
final tokenInfo = json["tokenInfo"]; final tokenInfo = json["tokenInfo"];
@ -86,21 +27,34 @@ class Turn14StatusModel {
return Turn14StatusModel( return Turn14StatusModel(
userId: json["userid"]?.toString() ?? "", userId: json["userid"]?.toString() ?? "",
hasCredentials: json["hasCredentials"] ?? false, hasCredentials: json["hasCredentials"] ?? false,
clientId: credentials?["turn14clientid"], clientId: credentials?["turn14clientid"],
clientSecret: credentials?["turn14clientsecret"], clientSecret: credentials?["turn14clientsecret"],
accessToken: tokenInfo?["access_token"], accessToken: tokenInfo?["access_token"],
expiresIn: tokenInfo?["expires_in"], expiresIn: tokenInfo?["expires_in"]?.toString(),
code: json["code"], code: json["code"],
message: json["message"], message: json["message"],
); );
} }
/// MODEL ENTITY (CLEAN ARCH) Map<String, dynamic> toJson() => {
Turn14StatusEntity toEntity() { "userid": userId,
return Turn14StatusEntity( "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, userId: userId,
hasCredentials: hasCredentials, hasCredentials: hasCredentials,
clientId: clientId, clientId: clientId,

View File

@ -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<EbayEntity?> 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<EbayEntity?> 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<String, dynamic>) return null;
final storeJson = data['store'];
if (storeJson == null || storeJson is! Map<String, dynamic>) return null;
// 5 Convert to entity
final storeEntity = EbayEntity.fromJson(storeJson);
return storeEntity;
}
}

View File

@ -32,26 +32,26 @@ class TokenRepository {
/// 🔄 Refresh Token API /// 🔄 Refresh Token API
Future<String> _refreshToken(String userId) async { Future<String> _refreshToken(String userId) async {
final response = await apiService.post( final response = await apiService.post(ApiEndpoints.getToken, {
ApiEndpoints.getToken, "userid": userId,
{"userid": userId}, });
);
final data = response.data; final data = response.data;
final code = data["code"];
if (data["code"] != "TOKEN_UPDATED") {
throw Exception(data["message"]);
}
final token = data["access_token"]; final token = data["access_token"];
/// Save New Token + Timestamp 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: _tokenKey, value: token);
await _storage.write( await _storage.write(
key: _tokenTimeKey, key: _tokenTimeKey,
value: DateTime.now().toIso8601String(), value: DateTime.now().toIso8601String(),
); );
}
return token; return token;
} else {
throw Exception("Failed to get Turn14 token: ${data["message"]}");
}
} }
} }

View File

@ -37,7 +37,7 @@ class Turn14RepositoryImpl implements Turn14Repository {
if (data['code'] == "TURN14_SAVED") { if (data['code'] == "TURN14_SAVED") {
/// Convert Response Entity /// Convert Response Entity
return Turn14Response.fromJson(data).toEntity(); return Turn14StatusModel.fromJson(data).toEntity();
} else { } else {
throw Exception( throw Exception(
data['message'] ?? "Failed to save Turn14 credentials", data['message'] ?? "Failed to save Turn14 credentials",
@ -53,7 +53,7 @@ class Turn14RepositoryImpl implements Turn14Repository {
} }
} }
Future<Turn14StatusEntity> status(String userId) async { Future<Turn14Entity> status(String userId) async {
try { try {
final response = await _apiService.post(ApiEndpoints.turn14Status, { final response = await _apiService.post(ApiEndpoints.turn14Status, {
"userid": userId, "userid": userId,

View File

@ -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<String, dynamic> 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<String, dynamic> 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,
};
}
}

View File

@ -2,16 +2,32 @@ class Turn14Entity {
final String code; final String code;
final String message; final String message;
final String userId; 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({ const Turn14Entity({
required this.code, required this.code,
required this.message, required this.message,
required this.userId, 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 { class Turn14StatusEntity {
final String userId; final String userId;
final bool hasCredentials; final bool hasCredentials;

View File

@ -6,8 +6,6 @@ import 'package:autos/presentation/screens/auth/login_screen.dart';
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart'; import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
// ADD THIS IMPORT
import 'package:onesignal_flutter/onesignal_flutter.dart'; import 'package:onesignal_flutter/onesignal_flutter.dart';
void main() { void main() {
@ -18,7 +16,6 @@ void main() {
OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085"); OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085");
OneSignal.Notifications.requestPermission(false); OneSignal.Notifications.requestPermission(false);
// KEEP YOUR ORIGINAL LOGIC
runApp(const ProviderScope(child: MyApp())); runApp(const ProviderScope(child: MyApp()));
} }

View File

@ -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<ApiService>((ref) => ApiService());
/// TOKEN REPOSITORY PROVIDER
final _tokenRepositoryProvider = Provider<TokenRepository>((ref) {
final api = ref.read(ebayApiServiceProvider);
return TokenRepository(api);
});
/// REPOSITORY PROVIDER
final ebayRepositoryProvider = Provider<EbayRepositoryImpl>((ref) {
final api = ref.read(ebayApiServiceProvider);
final tokenRepo = ref.read(_tokenRepositoryProvider);
return EbayRepositoryImpl(api, tokenRepo);
});
/// STATE NOTIFIER PROVIDER
final ebayProvider =
StateNotifierProvider<EbayNotifier, AsyncValue<EbayEntity?>>((ref) {
final repo = ref.read(ebayRepositoryProvider);
return EbayNotifier(repo);
});
/// NOTIFIER
class EbayNotifier extends StateNotifier<AsyncValue<EbayEntity?>> {
final EbayRepositoryImpl repository;
EbayNotifier(this.repository) : super(const AsyncValue.loading()) {
fetchStore();
}
/// FETCH STORE
Future<void> 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<EbayEntity>
// 3 Update state
state = AsyncValue.data(store);
} catch (e, st) {
debugPrint("❌ EBAY FETCH ERROR: $e");
state = AsyncValue.error(e, st);
}
}
}

View File

@ -8,18 +8,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart'; import 'package:flutter_riverpod/legacy.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// ------------------------------------------------------------ /// ------------------------------------------------------------
/// Service Providers /// Service Providers
/// ------------------------------------------------------------ /// ------------------------------------------------------------
final turn14ApiServiceProvider = final turn14ApiServiceProvider = Provider<ApiService>(
Provider<ApiService>((ref) => ApiService()); (ref) => ApiService(),
);
final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>((ref) {
return Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider));
});
final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>(
(ref) => Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider)),
);
/// ------------------------------------------------------------ /// ------------------------------------------------------------
/// Turn14 Notifier /// Turn14 Notifier
@ -27,14 +26,15 @@ final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>((ref) {
class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> { class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
final Turn14RepositoryImpl repository; 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)); Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
// ------------------------------------------------------------
/// Save Turn14 credentials // Save Turn14 credentials (API + local)
// ------------------------------------------------------------
Future<void> saveCredentials({ Future<void> saveCredentials({
required String userId, required String userId,
required String clientId, required String clientId,
@ -43,50 +43,127 @@ class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
try { try {
final response = await repository.save( final Turn14Entity entity = await repository.save(
userId: userId, userId: userId,
clientId: clientId, clientId: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
); );
// if (response is Turn14Response) { // Convert entity model (for persistence)
// await _storage.write( final model = Turn14StatusModel(
// key: _turn14StorageKey, userId: entity.userId,
// value: response.toRawJson(), 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) { } catch (e, st) {
state = AsyncValue.error(e, st); state = AsyncValue.error(e, st);
} }
} }
/// Load saved Turn14 credentials // ------------------------------------------------------------
// Load Turn14 from LOCAL storage
// ------------------------------------------------------------
Future<void> loadSavedCredentials() async { Future<void> loadSavedCredentials() async {
try {
final saved = await _storage.read(key: _turn14StorageKey); final saved = await _storage.read(key: _turn14StorageKey);
if (saved == null) return; if (saved == null) {
state = const AsyncValue.data(null);
final decoded = jsonDecode(saved); return;
final model = Turn14Response.fromJson(decoded);
state = AsyncValue.data(model as Turn14Entity?);
} }
/// Clear saved Turn14 data final decoded = jsonDecode(saved);
final model = Turn14StatusModel.fromJson(decoded);
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);
}
}
// ------------------------------------------------------------
// Load Turn14 status from API + persist locally
// ------------------------------------------------------------
Future<void> 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<void> clear() async { Future<void> clear() async {
await _storage.delete(key: _turn14StorageKey); await _storage.delete(key: _turn14StorageKey);
state = const AsyncValue.data(null); state = const AsyncValue.data(null);
} }
} }
/// ------------------------------------------------------------ /// ------------------------------------------------------------
/// Riverpod Provider /// Riverpod Provider
/// ------------------------------------------------------------ /// ------------------------------------------------------------
final turn14Provider = final turn14Provider =
StateNotifierProvider<Turn14Notifier, AsyncValue<Turn14Entity?>>((ref) { StateNotifierProvider<Turn14Notifier, AsyncValue<Turn14Entity?>>(
final repository = ref.read(turn14RepositoryProvider); (ref) => Turn14Notifier(ref.read(turn14RepositoryProvider)),
return Turn14Notifier(repository); );
});

View File

@ -17,8 +17,9 @@ final userRepositoryProvider = Provider<UserRepositoryImpl>(
); );
/// SINGLE GLOBAL USER PROVIDER /// SINGLE GLOBAL USER PROVIDER
final userProvider = final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) { ref,
) {
final repo = ref.read(userRepositoryProvider); final repo = ref.read(userRepositoryProvider);
return UserNotifier(ref, repo); return UserNotifier(ref, repo);
}); });
@ -34,8 +35,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
AuthAction lastAction = AuthAction.idle; AuthAction lastAction = AuthAction.idle;
UserNotifier(this.ref, this.repository) UserNotifier(this.ref, this.repository) : super(const AsyncValue.data(null));
: super(const AsyncValue.data(null));
// AUTO LOGIN ON APP START // AUTO LOGIN ON APP START
Future<void> loadUserFromStorage() async { Future<void> loadUserFromStorage() async {
@ -94,10 +94,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
final userModel = UserModel.fromEntity(user); final userModel = UserModel.fromEntity(user);
// 3 Store safely // 3 Store safely
await _storage.write( await _storage.write(key: _userKey, value: userModel.toRawJson());
key: _userKey,
value: userModel.toRawJson(),
);
// 4 Update provider state // 4 Update provider state
state = AsyncValue.data(user); state = AsyncValue.data(user);
@ -106,10 +103,13 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
} }
} }
// LOGOUT // LOGOUT
Future<void> logout() async { Future<void> 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); state = const AsyncValue.data(null);
} }

View File

@ -1,33 +1,42 @@
import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/core/theme/app_typography.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:flutter/material.dart';
import 'package:autos/core/widgets/hamburger_button.dart'; import 'package:autos/core/widgets/hamburger_button.dart';
import 'package:autos/core/widgets/side_menu.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}); const EbayScreen({super.key});
@override @override
State<EbayScreen> createState() => _EbayScreenState(); ConsumerState<EbayScreen> createState() => _EbayScreenState();
} }
class _EbayScreenState extends State<EbayScreen> { class _EbayScreenState extends ConsumerState<EbayScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
String selected = "ebay";
@override
void initState() {
super.initState();
/// Fetch ONCE
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(ebayProvider.notifier).fetchStore();
});
}
@override @override
Widget build(BuildContext context) { 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( return Scaffold(
key: _scaffoldKey, key: scaffoldKey,
drawer: SideMenu( drawer: SideMenu(selected: "ebay", onItemSelected: (_) {}),
selected: selected,
onItemSelected: (key) {
setState(() => selected = key);
},
),
// backgroundColor: const Color(0xFFEFFAFF),
body: Stack( body: Stack(
children: [ children: [
/// TITLE /// TITLE
@ -43,51 +52,59 @@ class _EbayScreenState extends State<EbayScreen> {
), ),
), ),
/// MAIN BOX UI /// CONTENT
Center( Positioned.fill(
child: Container( top: topPadding + 60,
width: MediaQuery.of(context).size.width * 0.90, child: LayoutBuilder(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 40), builder: (context, constraints) {
margin: EdgeInsets.only(top: topPadding + 60), return SingleChildScrollView(
decoration: BoxDecoration( padding: const EdgeInsets.symmetric(vertical: 24),
color: Colors.white, child: Center(
borderRadius: BorderRadius.circular(25), child: ConstrainedBox(
boxShadow: [ constraints: const BoxConstraints(maxWidth: 720),
BoxShadow( child: storeAsync.when(
color: Colors.black12, loading: () =>
blurRadius: 25, const Center(child: CircularProgressIndicator()),
offset: const Offset(0, 12), error: (e, _) => _errorCard(e.toString()),
data: (store) => store == null
? _connectCard(context)
: _connectedCard(context, store),
), ),
),
),
);
},
),
),
HamburgerButton(scaffoldKey: scaffoldKey),
], ],
), ),
child: Column( );
mainAxisSize: MainAxisSize.min, }
// ---------------------------------------------------------------------------
// CONNECT (NOT CONNECTED)
// ---------------------------------------------------------------------------
Widget _connectCard(BuildContext context) {
return _cardWrapper(
Column(
children: [ children: [
/// Description const Text(
Text(
"Connect your eBay store to enable product sync, inventory updates, and order flow.", "Connect your eBay store to enable product sync, inventory updates, and order flow.",
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: const TextStyle( style: TextStyle(fontSize: 15, color: Colors.black54, height: 1.5),
fontSize: 15,
color: Colors.black54,
height: 1.5,
), ),
),
const SizedBox(height: 30), const SizedBox(height: 30),
/// BUTTON
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {},
// TODO: Add eBay authorization flow
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF), backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(12),
), ),
), ),
child: const Text( child: const Text(
@ -95,28 +112,252 @@ class _EbayScreenState extends State<EbayScreen> {
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w600, 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),
], ],
), ),
); );
} }
// ---------------------------------------------------------------------------
// 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(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 6),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: Colors.green.shade300),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.check_circle, color: Colors.green, size: 18),
SizedBox(width: 6),
Text(
"Connected",
style: TextStyle(
color: Colors.green,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
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,
);
}
} }

View File

@ -18,18 +18,56 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'turn14'; String selected = 'turn14';
// controllers
final TextEditingController clientIdController = TextEditingController(); final TextEditingController clientIdController = TextEditingController();
final TextEditingController clientSecretController = 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final asyncUser = ref.watch(userProvider); final asyncUser = ref.watch(userProvider);
final turn14State = ref.watch(turn14Provider); final turn14State = ref.watch(turn14Provider);
final user = asyncUser.value; final user = asyncUser.value;
final double topPadding = MediaQuery.of(context).padding.top + 16; 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( return Scaffold(
key: _scaffoldKey, key: _scaffoldKey,
drawer: SideMenu( drawer: SideMenu(
@ -44,27 +82,41 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
top: topPadding, top: topPadding,
left: 0, left: 0,
right: 0, right: 0,
child: Row( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
"Turn14 Settings", "Turn14 Settings",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700), 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( SingleChildScrollView(
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20), padding: EdgeInsets.fromLTRB(16, topPadding + 80, 16, 20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
Text("", style: const TextStyle(fontSize: 28)), const Text("", style: TextStyle(fontSize: 28)),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
@ -82,13 +134,10 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
_inputField(label: "Client ID", controller: clientIdController), _inputField(label: "Client ID", controller: clientIdController),
const SizedBox(height: 20), const SizedBox(height: 20),
_passwordField( _passwordField(label: "Secret Key"),
label: "Secret Key",
controller: clientSecretController,
),
const SizedBox(height: 25), const SizedBox(height: 25),
/// SAVE BUTTON /// SAVE BUTTON
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@ -99,18 +148,18 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
), ),
backgroundColor: const Color(0xFF00C9FF), backgroundColor: const Color(0xFF00C9FF),
), ),
onPressed: turn14State.isLoading onPressed: () async {
? null if (turn14State.isLoading) {
: () async { return; // prevent double click
}
if (isConnected) {
Fluttertoast.showToast(msg: "✅ Already connected");
return;
}
if (user == null) { if (user == null) {
Fluttertoast.showToast( Fluttertoast.showToast(msg: "⚠️ User not found");
msg: "⚠️ User not found",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.red,
textColor: Colors.white,
fontSize: 16.0,
);
return; return;
} }
@ -118,11 +167,6 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
clientSecretController.text.trim().isEmpty) { clientSecretController.text.trim().isEmpty) {
Fluttertoast.showToast( Fluttertoast.showToast(
msg: "⚠️ Please fill all fields", msg: "⚠️ Please fill all fields",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.orange,
textColor: Colors.white,
fontSize: 16.0,
); );
return; return;
} }
@ -132,35 +176,31 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
.saveCredentials( .saveCredentials(
userId: user.id, userId: user.id,
clientId: clientIdController.text.trim(), clientId: clientIdController.text.trim(),
clientSecret: clientSecret: clientSecretController.text.trim(),
clientSecretController.text.trim(),
); );
Fluttertoast.showToast( Fluttertoast.showToast(
msg: "✅ Turn14 Credentials Saved Successfully", msg: "✅ Turn14 Connected Successfully",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
); );
}, },
child: turn14State.isLoading child: turn14State.isLoading
? const CircularProgressIndicator(color: Colors.white) ? const SizedBox(
height: 22,
width: 22,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text( : const Text(
"Save Credentials", "Save Credentials",
style: TextStyle( style: TextStyle(fontWeight: FontWeight.bold),
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
), ),
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_infoBox(isConnected),
_infoBox(),
const SizedBox(height: 20), const SizedBox(height: 20),
_tipsBox(), _tipsBox(),
], ],
@ -173,7 +213,8 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
); );
} }
/// UI COMPONENTS /// ---------------- UI COMPONENTS ----------------
Widget _inputField({ Widget _inputField({
required String label, required String label,
required TextEditingController controller, required TextEditingController controller,
@ -198,21 +239,14 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
); );
} }
Widget _passwordField({ Widget _passwordField({required String label}) {
required String label,
required TextEditingController controller,
}) {
bool _obscure = true;
return StatefulBuilder(
builder: (context, setStateSB) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(label, style: const TextStyle(fontWeight: FontWeight.w600)), Text(label, style: const TextStyle(fontWeight: FontWeight.w600)),
const SizedBox(height: 8), const SizedBox(height: 8),
TextField( TextField(
controller: controller, controller: clientSecretController,
obscureText: _obscure, obscureText: _obscure,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
@ -222,34 +256,32 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
borderSide: BorderSide.none, borderSide: BorderSide.none,
), ),
suffixIcon: IconButton( suffixIcon: IconButton(
icon: Icon( icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility),
_obscure ? Icons.visibility_off : Icons.visibility, onPressed: () => setState(() => _obscure = !_obscure),
),
onPressed: () => setStateSB(() => _obscure = !_obscure),
), ),
), ),
), ),
], ],
); );
},
);
} }
Widget _infoBox() { Widget _infoBox(bool isConnected) {
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFE8F1FF), color: isConnected ? Colors.green.shade50 : const Color(0xFFE8F1FF),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
children: const [ children: [
Icon(Icons.info, color: Colors.blue), Icon(Icons.info, color: isConnected ? Colors.green : Colors.blue),
SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
"No credentials saved yet.", isConnected
style: TextStyle(color: Colors.black87), ? "Existing Turn14 credentials loaded."
: "No credentials saved yet.",
style: const TextStyle(color: Colors.black87),
), ),
], ],
), ),
@ -264,9 +296,9 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
color: const Color(0xFFE8FCFF), color: const Color(0xFFE8FCFF),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Column( child: const Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: const [ children: [
Text( Text(
"💡 Connection Tips", "💡 Connection Tips",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),

View File

@ -8,9 +8,11 @@ import Foundation
import flutter_inappwebview_macos import flutter_inappwebview_macos
import flutter_secure_storage_macos import flutter_secure_storage_macos
import path_provider_foundation import path_provider_foundation
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
} }

View File

@ -789,6 +789,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" 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: win32:
dependency: transitive dependency: transitive
description: description:

View File

@ -22,6 +22,7 @@ dependencies:
youtube_player_flutter: ^9.1.3 youtube_player_flutter: ^9.1.3
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4
onesignal_flutter: ^5.3.4 onesignal_flutter: ^5.3.4
webview_flutter: ^4.13.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: