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 {
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 = "../.."
}

View File

@ -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

View File

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

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';
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,

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
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
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"]}");
}
}
}

View File

@ -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,

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 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;

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: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()));
}

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_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 {
try {
final saved = await _storage.read(key: _turn14StorageKey);
if (saved == null) return;
final decoded = jsonDecode(saved);
final model = Turn14Response.fromJson(decoded);
state = AsyncValue.data(model as Turn14Entity?);
if (saved == null) {
state = const AsyncValue.data(null);
return;
}
/// 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 {
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)),
);

View File

@ -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 {
@ -81,7 +81,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
String email,
String password,
String phone,
) async {
) async {
lastAction = AuthAction.signup;
try {
@ -94,22 +94,22 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
final userModel = UserModel.fromEntity(user);
// 3 Store safely
await _storage.write(
key: _userKey,
value: userModel.toRawJson(),
);
await _storage.write(key: _userKey, value: userModel.toRawJson());
// 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);
}

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/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,51 +52,59 @@ class _EbayScreenState extends State<EbayScreen> {
),
),
/// MAIN BOX UI
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),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 25,
offset: const Offset(0, 12),
/// 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),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
);
}
// ---------------------------------------------------------------------------
// CONNECT (NOT CONNECTED)
// ---------------------------------------------------------------------------
Widget _connectCard(BuildContext context) {
return _cardWrapper(
Column(
children: [
/// Description
Text(
const 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,
style: TextStyle(fontSize: 15, color: Colors.black54, height: 1.5),
),
),
const SizedBox(height: 30),
/// BUTTON
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// TODO: Add eBay authorization flow
},
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00CFFF),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
@ -95,28 +112,252 @@ class _EbayScreenState extends State<EbayScreen> {
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),
],
),
);
}
// ---------------------------------------------------------------------------
// 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>();
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,18 +148,18 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
),
backgroundColor: const Color(0xFF00C9FF),
),
onPressed: turn14State.isLoading
? null
: () async {
onPressed: () async {
if (turn14State.isLoading) {
return; // prevent double click
}
if (isConnected) {
Fluttertoast.showToast(msg: "✅ Already connected");
return;
}
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,
);
Fluttertoast.showToast(msg: "⚠️ User not found");
return;
}
@ -118,11 +167,6 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
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;
}
@ -132,35 +176,31 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
.saveCredentials(
userId: user.id,
clientId: clientIdController.text.trim(),
clientSecret:
clientSecretController.text.trim(),
clientSecret: clientSecretController.text.trim(),
);
Fluttertoast.showToast(
msg: "✅ Turn14 Credentials Saved Successfully",
toastLength: Toast.LENGTH_LONG,
gravity: ToastGravity.BOTTOM,
backgroundColor: Colors.green,
textColor: Colors.white,
fontSize: 16.0,
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,21 +239,14 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
);
}
Widget _passwordField({
required String label,
required TextEditingController controller,
}) {
bool _obscure = true;
return StatefulBuilder(
builder: (context, setStateSB) {
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: controller,
controller: clientSecretController,
obscureText: _obscure,
decoration: InputDecoration(
filled: true,
@ -222,34 +256,32 @@ class _Turn14ScreenState extends ConsumerState<Turn14Screen> {
borderSide: BorderSide.none,
),
suffixIcon: IconButton(
icon: Icon(
_obscure ? Icons.visibility_off : Icons.visibility,
),
onPressed: () => setStateSB(() => _obscure = !_obscure),
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),

View File

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

View File

@ -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:

View File

@ -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: