ebay screen connected to backend.
This commit is contained in:
parent
c6f2b15453
commit
a7d57dfa72
@ -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 = "../.."
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
26
lib/core/utils/date_time_utils.dart
Normal file
26
lib/core/utils/date_time_utils.dart
Normal 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}";
|
||||
}
|
||||
}
|
||||
56
lib/core/utils/ebay_webview_screen.dart
Normal file
56
lib/core/utils/ebay_webview_screen.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<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 {
|
||||
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<String, dynamic> 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<String, dynamic> 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,
|
||||
|
||||
54
lib/data/repositories/ebay_repository_impl.dart
Normal file
54
lib/data/repositories/ebay_repository_impl.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -32,26 +32,26 @@ class TokenRepository {
|
||||
|
||||
/// 🔄 Refresh Token API
|
||||
Future<String> _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"]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Turn14StatusEntity> status(String userId) async {
|
||||
Future<Turn14Entity> status(String userId) async {
|
||||
try {
|
||||
final response = await _apiService.post(ApiEndpoints.turn14Status, {
|
||||
"userid": userId,
|
||||
|
||||
45
lib/domain/entities/ebay.dart
Normal file
45
lib/domain/entities/ebay.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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()));
|
||||
}
|
||||
|
||||
|
||||
76
lib/presentation/providers/ebay_provider.dart
Normal file
76
lib/presentation/providers/ebay_provider.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<ApiService>((ref) => ApiService());
|
||||
|
||||
final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>((ref) {
|
||||
return Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider));
|
||||
});
|
||||
final turn14ApiServiceProvider = Provider<ApiService>(
|
||||
(ref) => ApiService(),
|
||||
);
|
||||
|
||||
final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>(
|
||||
(ref) => Turn14RepositoryImpl(ref.read(turn14ApiServiceProvider)),
|
||||
);
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
/// Turn14 Notifier
|
||||
@ -27,14 +26,15 @@ final turn14RepositoryProvider = Provider<Turn14RepositoryImpl>((ref) {
|
||||
|
||||
class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
|
||||
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<void> saveCredentials({
|
||||
required String userId,
|
||||
required String clientId,
|
||||
@ -43,50 +43,127 @@ class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
|
||||
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<void> 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<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 {
|
||||
await _storage.delete(key: _turn14StorageKey);
|
||||
state = const AsyncValue.data(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// ------------------------------------------------------------
|
||||
/// Riverpod Provider
|
||||
/// ------------------------------------------------------------
|
||||
|
||||
final turn14Provider =
|
||||
StateNotifierProvider<Turn14Notifier, AsyncValue<Turn14Entity?>>((ref) {
|
||||
final repository = ref.read(turn14RepositoryProvider);
|
||||
return Turn14Notifier(repository);
|
||||
});
|
||||
StateNotifierProvider<Turn14Notifier, AsyncValue<Turn14Entity?>>(
|
||||
(ref) => Turn14Notifier(ref.read(turn14RepositoryProvider)),
|
||||
);
|
||||
|
||||
@ -17,8 +17,9 @@ final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
||||
);
|
||||
|
||||
/// SINGLE GLOBAL USER PROVIDER
|
||||
final userProvider =
|
||||
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||
final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((
|
||||
ref,
|
||||
) {
|
||||
final repo = ref.read(userRepositoryProvider);
|
||||
return UserNotifier(ref, repo);
|
||||
});
|
||||
@ -34,8 +35,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
|
||||
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<void> loadUserFromStorage() async {
|
||||
@ -65,51 +65,51 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
||||
// 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<void> signup(
|
||||
String name,
|
||||
String email,
|
||||
String password,
|
||||
String phone,
|
||||
) async {
|
||||
lastAction = AuthAction.signup;
|
||||
// SIGNUP
|
||||
Future<void> 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<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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<EbayScreen> createState() => _EbayScreenState();
|
||||
ConsumerState<EbayScreen> createState() => _EbayScreenState();
|
||||
}
|
||||
|
||||
class _EbayScreenState extends State<EbayScreen> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
String selected = "ebay";
|
||||
class _EbayScreenState extends ConsumerState<EbayScreen> {
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
|
||||
@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<EbayScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,18 +18,56 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||
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<Turn14Screen> {
|
||||
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<Turn14Screen> {
|
||||
_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<Turn14Screen> {
|
||||
),
|
||||
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<Turn14Screen> {
|
||||
);
|
||||
}
|
||||
|
||||
/// UI COMPONENTS
|
||||
/// ---------------- UI COMPONENTS ----------------
|
||||
|
||||
Widget _inputField({
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
@ -198,58 +239,49 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
|
||||
);
|
||||
}
|
||||
|
||||
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<Turn14Screen> {
|
||||
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),
|
||||
|
||||
@ -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"))
|
||||
}
|
||||
|
||||
32
pubspec.lock
32
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:
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user