push notification implemented.
This commit is contained in:
parent
064e753fb9
commit
c6f2b15453
@ -5,6 +5,7 @@ import 'package:autos/presentation/screens/auth/sign_up_screen.dart';
|
|||||||
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
import 'package:autos/presentation/screens/brands/brands_screen.dart';
|
||||||
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
||||||
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
import 'package:autos/presentation/screens/ebay/ebay_screen.dart';
|
||||||
|
import 'package:autos/presentation/screens/imports/imports_screen.dart';
|
||||||
import 'package:autos/presentation/screens/my_account/account_screen.dart';
|
import 'package:autos/presentation/screens/my_account/account_screen.dart';
|
||||||
import 'package:autos/presentation/screens/pricing/pricing_screen.dart';
|
import 'package:autos/presentation/screens/pricing/pricing_screen.dart';
|
||||||
import 'package:autos/presentation/screens/products/products_screen.dart';
|
import 'package:autos/presentation/screens/products/products_screen.dart';
|
||||||
@ -73,6 +74,9 @@ class AppRouter {
|
|||||||
case AppRoutePaths.myAccount:
|
case AppRoutePaths.myAccount:
|
||||||
return slideRoute(AccountScreen());
|
return slideRoute(AccountScreen());
|
||||||
|
|
||||||
|
case AppRoutePaths.imports:
|
||||||
|
return slideRoute(ImportsScreen());
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return _defaultFallback(settings);
|
return _defaultFallback(settings);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,4 +18,5 @@ class AppRoutePaths {
|
|||||||
static const products = '/products';
|
static const products = '/products';
|
||||||
static const pricing = '/pricing';
|
static const pricing = '/pricing';
|
||||||
static const myAccount = '/myAccount';
|
static const myAccount = '/myAccount';
|
||||||
|
static const imports = '/imports';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ class AppTheme {
|
|||||||
// ✅ MAIN BRAND COLORS
|
// ✅ MAIN BRAND COLORS
|
||||||
static const Color primary = Color(0xFF00BFFF); // Main Blue
|
static const Color primary = Color(0xFF00BFFF); // Main Blue
|
||||||
static const Color lightBlue = Color(0xFFE8F7FF); // Sidebar highlight
|
static const Color lightBlue = Color(0xFFE8F7FF); // Sidebar highlight
|
||||||
static const Color background = Color(0xFFF4FBFE); // App background
|
static const Color background = Color(0xFFFFFFFF); // 0xFFF4FBFE App background
|
||||||
static const Color textDark = Color(0xFF1C1C1C);
|
static const Color textDark = Color(0xFF1C1C1C);
|
||||||
static const Color textGrey = Color(0xFF6F6F6F);
|
static const Color textGrey = Color(0xFF6F6F6F);
|
||||||
static const Color cardBorder = Color(0xFFAEE9FF);
|
static const Color cardBorder = Color(0xFFAEE9FF);
|
||||||
|
|||||||
@ -66,7 +66,7 @@ class SideMenu extends ConsumerWidget {
|
|||||||
_sectionHeader("MANAGE"),
|
_sectionHeader("MANAGE"),
|
||||||
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
|
||||||
_menuItem(context, "📦", "Products", AppRoutePaths.products),
|
_menuItem(context, "📦", "Products", AppRoutePaths.products),
|
||||||
_menuItem(context, "⬇️", "Imports", 'imports'),
|
_menuItem(context, "⬇️", "Imports", AppRoutePaths.imports),
|
||||||
|
|
||||||
// --- ACCOUNT ---
|
// --- ACCOUNT ---
|
||||||
_sectionHeader("ACCOUNT"),
|
_sectionHeader("ACCOUNT"),
|
||||||
|
|||||||
57
lib/data/models/import_product_model.dart
Normal file
57
lib/data/models/import_product_model.dart
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
|
||||||
|
class ImportProductModel {
|
||||||
|
final String id;
|
||||||
|
final String sku;
|
||||||
|
final String image;
|
||||||
|
final String name;
|
||||||
|
final String partNumber;
|
||||||
|
final String category;
|
||||||
|
final String subcategory;
|
||||||
|
final double price;
|
||||||
|
final int inventory;
|
||||||
|
final String offerStatus;
|
||||||
|
|
||||||
|
ImportProductModel({
|
||||||
|
required this.id,
|
||||||
|
required this.sku,
|
||||||
|
required this.image,
|
||||||
|
required this.name,
|
||||||
|
required this.partNumber,
|
||||||
|
required this.category,
|
||||||
|
required this.subcategory,
|
||||||
|
required this.price,
|
||||||
|
required this.inventory,
|
||||||
|
required this.offerStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ImportProductModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return ImportProductModel(
|
||||||
|
id: json['id']?.toString() ?? '',
|
||||||
|
sku: json['sku'] ?? '',
|
||||||
|
image: json['imgSrc'] ?? '',
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
partNumber: json['partNumber'] ?? '',
|
||||||
|
category: json['category'] ?? '',
|
||||||
|
subcategory: json['subcategory'] ?? '',
|
||||||
|
price: double.tryParse(json['price'].toString()) ?? 0,
|
||||||
|
inventory: json['inventory'] ?? 0,
|
||||||
|
offerStatus: json['offer_status'] ?? '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportProductEntity toEntity() {
|
||||||
|
return ImportProductEntity(
|
||||||
|
id: id,
|
||||||
|
sku: sku,
|
||||||
|
image: image,
|
||||||
|
name: name,
|
||||||
|
partNumber: partNumber,
|
||||||
|
category: category,
|
||||||
|
subcategory: subcategory,
|
||||||
|
price: price,
|
||||||
|
inventory: inventory,
|
||||||
|
offerStatus: offerStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -63,7 +63,9 @@ class Turn14StatusModel {
|
|||||||
final String? clientId;
|
final String? clientId;
|
||||||
final String? clientSecret;
|
final String? clientSecret;
|
||||||
final String? accessToken;
|
final String? accessToken;
|
||||||
final String? expiresIn;
|
final int? expiresIn; // ✅ FIXED: should be int
|
||||||
|
final String? code; // ✅ ADDED
|
||||||
|
final String? message; // ✅ ADDED
|
||||||
|
|
||||||
Turn14StatusModel({
|
Turn14StatusModel({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
@ -72,19 +74,31 @@ class Turn14StatusModel {
|
|||||||
this.clientSecret,
|
this.clientSecret,
|
||||||
this.accessToken,
|
this.accessToken,
|
||||||
this.expiresIn,
|
this.expiresIn,
|
||||||
|
this.code,
|
||||||
|
this.message,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// ✅ SAFE FROM JSON
|
||||||
factory Turn14StatusModel.fromJson(Map<String, dynamic> json) {
|
factory Turn14StatusModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final credentials = json["credentials"];
|
||||||
|
final tokenInfo = json["tokenInfo"];
|
||||||
|
|
||||||
return Turn14StatusModel(
|
return Turn14StatusModel(
|
||||||
userId: json["userid"],
|
userId: json["userid"]?.toString() ?? "",
|
||||||
hasCredentials: json["hasCredentials"] ?? false,
|
hasCredentials: json["hasCredentials"] ?? false,
|
||||||
clientId: json["credentials"]?["turn14clientid"],
|
|
||||||
clientSecret: json["credentials"]?["turn14clientsecret"],
|
clientId: credentials?["turn14clientid"],
|
||||||
accessToken: json["tokenInfo"]?["access_token"],
|
clientSecret: credentials?["turn14clientsecret"],
|
||||||
expiresIn: json["tokenInfo"]?["expires_in"],
|
|
||||||
|
accessToken: tokenInfo?["access_token"],
|
||||||
|
expiresIn: tokenInfo?["expires_in"],
|
||||||
|
|
||||||
|
code: json["code"],
|
||||||
|
message: json["message"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ✅ MODEL → ENTITY (CLEAN ARCH)
|
||||||
Turn14StatusEntity toEntity() {
|
Turn14StatusEntity toEntity() {
|
||||||
return Turn14StatusEntity(
|
return Turn14StatusEntity(
|
||||||
userId: userId,
|
userId: userId,
|
||||||
|
|||||||
@ -1,7 +1,115 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:autos/domain/entities/user.dart';
|
import 'package:autos/domain/entities/user.dart';
|
||||||
|
|
||||||
|
/// ✅ STORE MODEL
|
||||||
|
class StoreModel {
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final String url;
|
||||||
|
final String urlPath;
|
||||||
|
final String? lastOpenedTime;
|
||||||
|
final String? lastOpenedTimeRaw;
|
||||||
|
final String? logoUrl;
|
||||||
|
|
||||||
|
const StoreModel({
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.url,
|
||||||
|
required this.urlPath,
|
||||||
|
this.lastOpenedTime,
|
||||||
|
this.lastOpenedTimeRaw,
|
||||||
|
this.logoUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory StoreModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return StoreModel(
|
||||||
|
name: json['name'] ?? '',
|
||||||
|
description: json['description'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
urlPath: json['urlPath'] ?? '',
|
||||||
|
lastOpenedTime: json['lastOpenedTime'],
|
||||||
|
lastOpenedTimeRaw: json['lastOpenedTimeRaw'],
|
||||||
|
logoUrl: json['logo']?['url'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'description': description,
|
||||||
|
'url': url,
|
||||||
|
'urlPath': urlPath,
|
||||||
|
'lastOpenedTime': lastOpenedTime,
|
||||||
|
'lastOpenedTimeRaw': lastOpenedTimeRaw,
|
||||||
|
'logo': {
|
||||||
|
'url': logoUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ PAYMENT MODEL
|
||||||
|
class PaymentModel {
|
||||||
|
final int id;
|
||||||
|
final String email;
|
||||||
|
final String amount;
|
||||||
|
final String plan;
|
||||||
|
final String status;
|
||||||
|
final String? stripeSessionId;
|
||||||
|
final String? stripePaymentIntentId;
|
||||||
|
final String? subscriptionId;
|
||||||
|
final String? startDate;
|
||||||
|
final String? endDate;
|
||||||
|
|
||||||
|
const PaymentModel({
|
||||||
|
required this.id,
|
||||||
|
required this.email,
|
||||||
|
required this.amount,
|
||||||
|
required this.plan,
|
||||||
|
required this.status,
|
||||||
|
this.stripeSessionId,
|
||||||
|
this.stripePaymentIntentId,
|
||||||
|
this.subscriptionId,
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaymentModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PaymentModel(
|
||||||
|
id: json['id'] ?? 0,
|
||||||
|
email: json['email'] ?? '',
|
||||||
|
amount: json['amount'] ?? '',
|
||||||
|
plan: json['plan'] ?? '',
|
||||||
|
status: json['status'] ?? '',
|
||||||
|
stripeSessionId: json['stripeSessionId'],
|
||||||
|
stripePaymentIntentId: json['stripePaymentIntentId'],
|
||||||
|
subscriptionId: json['subscriptionId'],
|
||||||
|
startDate: json['startDate'],
|
||||||
|
endDate: json['endDate'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'id': id,
|
||||||
|
'email': email,
|
||||||
|
'amount': amount,
|
||||||
|
'plan': plan,
|
||||||
|
'status': status,
|
||||||
|
'stripeSessionId': stripeSessionId,
|
||||||
|
'stripePaymentIntentId': stripePaymentIntentId,
|
||||||
|
'subscriptionId': subscriptionId,
|
||||||
|
'startDate': startDate,
|
||||||
|
'endDate': endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ MAIN USER MODEL
|
||||||
class UserModel extends User {
|
class UserModel extends User {
|
||||||
|
final StoreModel? store;
|
||||||
|
final PaymentModel? payment;
|
||||||
|
|
||||||
const UserModel({
|
const UserModel({
|
||||||
required super.id,
|
required super.id,
|
||||||
required super.name,
|
required super.name,
|
||||||
@ -12,25 +120,37 @@ class UserModel extends User {
|
|||||||
super.phoneNumber,
|
super.phoneNumber,
|
||||||
super.message,
|
super.message,
|
||||||
super.code,
|
super.code,
|
||||||
|
this.store,
|
||||||
|
this.payment,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// ✅ FROM API JSON
|
/// ✅ FROM API JSON
|
||||||
factory UserModel.fromJson(Map<String, dynamic> json) {
|
factory UserModel.fromJson(Map<String, dynamic> json) {
|
||||||
final payment = json['payment'] ?? {};
|
final paymentJson = json['payment'];
|
||||||
|
final storeJson = json['store'];
|
||||||
|
|
||||||
|
final payment =
|
||||||
|
paymentJson != null ? PaymentModel.fromJson(paymentJson) : null;
|
||||||
|
|
||||||
|
final store =
|
||||||
|
storeJson != null ? StoreModel.fromJson(storeJson) : null;
|
||||||
|
|
||||||
return UserModel(
|
return UserModel(
|
||||||
id: json['userid'] ?? json['id']?.toString() ?? '',
|
id: json['userid'] ?? json['id']?.toString() ?? '',
|
||||||
name: json['name'] ?? '',
|
name: json['name'] ?? '',
|
||||||
email: json['email'] ?? '',
|
email: json['email'] ?? '',
|
||||||
role: json['role'] ?? '',
|
role: json['role'] ?? '',
|
||||||
plan: payment['plan'] ?? json['plan'],
|
plan: payment?.plan ?? json['plan'],
|
||||||
paymentStatus: payment['status'] ?? json['paymentStatus'],
|
paymentStatus: payment?.status ?? json['paymentStatus'],
|
||||||
phoneNumber: json['phonenumber'] ?? '',
|
phoneNumber: json['phonenumber'] ?? '',
|
||||||
message: json['message'] ?? '',
|
message: json['message'] ?? '',
|
||||||
code: json['code'] ?? '',
|
code: json['code'] ?? '',
|
||||||
|
store: store,
|
||||||
|
payment: payment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ✅ ✅ ✅ THIS WAS MISSING — VERY IMPORTANT
|
/// ✅ FROM ENTITY (LOCAL CONVERSION)
|
||||||
factory UserModel.fromEntity(User user) {
|
factory UserModel.fromEntity(User user) {
|
||||||
return UserModel(
|
return UserModel(
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -45,7 +165,7 @@ class UserModel extends User {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ✅ TO JSON FOR STORAGE
|
/// ✅ TO JSON (FOR LOCAL STORAGE)
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'userid': id,
|
'userid': id,
|
||||||
@ -57,6 +177,8 @@ class UserModel extends User {
|
|||||||
'phonenumber': phoneNumber,
|
'phonenumber': phoneNumber,
|
||||||
'message': message,
|
'message': message,
|
||||||
'code': code,
|
'code': code,
|
||||||
|
'store': store?.toJson(),
|
||||||
|
'payment': payment?.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +191,6 @@ class UserModel extends User {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'UserModel(id: $id, name: $name, email: $email, role: $role, phone: $phoneNumber, plan: $plan, status: $paymentStatus, code: $code)';
|
return 'UserModel(id: $id, name: $name, email: $email, role: $role, plan: $plan, status: $paymentStatus)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
147
lib/data/repositories/imports_repository_impl.dart
Normal file
147
lib/data/repositories/imports_repository_impl.dart
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:autos/core/constants/api_endpoints.dart';
|
||||||
|
import 'package:autos/data/models/import_product_model.dart';
|
||||||
|
import 'package:autos/data/sources/remote/api_service.dart';
|
||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:autos/data/models/user_model.dart';
|
||||||
|
|
||||||
|
abstract class ImportsRepository {
|
||||||
|
Future<List<ImportProductEntity>> getUserProducts({
|
||||||
|
required int page,
|
||||||
|
required int pageSize,
|
||||||
|
String? category,
|
||||||
|
String? subcategory,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportsRepositoryImpl implements ImportsRepository {
|
||||||
|
final ApiService _apiService;
|
||||||
|
final _storage = const FlutterSecureStorage();
|
||||||
|
static const _userKey = 'logged_in_user';
|
||||||
|
|
||||||
|
ImportsRepositoryImpl(this._apiService);
|
||||||
|
|
||||||
|
/// ✅ TOKEN LOGIC (UNCHANGED)
|
||||||
|
Future<String> getTurn14AccessToken() async {
|
||||||
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
|
if (jsonString == null) {
|
||||||
|
throw Exception("User not found. Please login again.");
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = UserModel.fromJson(jsonDecode(jsonString));
|
||||||
|
|
||||||
|
final response = await _apiService.post(
|
||||||
|
ApiEndpoints.getToken,
|
||||||
|
{"userid": user.id},
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
String token;
|
||||||
|
|
||||||
|
if (data["code"] == "TOKEN_UPDATED") {
|
||||||
|
token = data["access_token"];
|
||||||
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
|
} else if (data["code"] == "TOKEN_VALID") {
|
||||||
|
token = data["access_token"];
|
||||||
|
await _storage.write(key: 'turn14_token', value: token);
|
||||||
|
token = await _storage.read(key: 'turn14_token') ?? '';
|
||||||
|
if (token.isEmpty) {
|
||||||
|
throw Exception("Token missing in storage");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception(data["message"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("✅ Turn14 Token: $token");
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅✅✅ FINAL: FETCH USER PRODUCTS WITH PAGINATION + FILTERS
|
||||||
|
@override
|
||||||
|
Future<List<ImportProductEntity>> getUserProducts({
|
||||||
|
required int page,
|
||||||
|
required int pageSize,
|
||||||
|
String? category,
|
||||||
|
String? subcategory,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final token = await getTurn14AccessToken();
|
||||||
|
final userId = await _getUserId();
|
||||||
|
|
||||||
|
/// ✅ CLEAN PARAMS (SKIPS "All")
|
||||||
|
final Map<String, dynamic> params = {
|
||||||
|
"userid": userId,
|
||||||
|
"page": page,
|
||||||
|
"pageSize": pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (category != null && category.isNotEmpty && category != "All") {
|
||||||
|
params["category"] = category;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcategory != null &&
|
||||||
|
subcategory.isNotEmpty &&
|
||||||
|
subcategory != "All") {
|
||||||
|
params["subcategory"] = subcategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint("📡 FETCH USER PRODUCTS");
|
||||||
|
debugPrint("➡️ PARAMS: $params");
|
||||||
|
|
||||||
|
final response = await _apiService.get(
|
||||||
|
"/api/user-products",
|
||||||
|
overrideBaseUrl: "https://ebay.backend.data4autos.com",
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
params: params,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint("✅ API STATUS: ${response.statusCode}");
|
||||||
|
debugPrint("✅ RAW RESPONSE: ${response.data}");
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception("Server error: ${response.statusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
|
||||||
|
/// ✅ API RETURNS `items`
|
||||||
|
if (data is! Map || data["items"] == null) {
|
||||||
|
throw Exception("Invalid API structure: 'items' key missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
final List list = data["items"];
|
||||||
|
|
||||||
|
debugPrint("✅ TOTAL ITEMS LOADED: ${list.length}");
|
||||||
|
|
||||||
|
return list
|
||||||
|
.map(
|
||||||
|
(json) => ImportProductModel.fromJson(json).toEntity(),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
debugPrint("❌ NETWORK ERROR: ${e.response?.data ?? e.message}");
|
||||||
|
throw Exception("Network error: ${e.response?.data ?? e.message}");
|
||||||
|
} catch (e, st) {
|
||||||
|
debugPrint("❌ PARSING ERROR: $e");
|
||||||
|
debugPrintStack(stackTrace: st);
|
||||||
|
throw Exception("Unexpected error: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ GET USER ID FROM STORAGE
|
||||||
|
Future<String> _getUserId() async {
|
||||||
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
|
if (jsonString == null) {
|
||||||
|
throw Exception("User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = UserModel.fromJson(jsonDecode(jsonString));
|
||||||
|
return user.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/domain/entities/import_product.dart
Normal file
25
lib/domain/entities/import_product.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
class ImportProductEntity {
|
||||||
|
final String id;
|
||||||
|
final String sku;
|
||||||
|
final String image;
|
||||||
|
final String name;
|
||||||
|
final String partNumber;
|
||||||
|
final String category;
|
||||||
|
final String subcategory;
|
||||||
|
final double price;
|
||||||
|
final int inventory;
|
||||||
|
final String offerStatus;
|
||||||
|
|
||||||
|
ImportProductEntity({
|
||||||
|
required this.id,
|
||||||
|
required this.sku,
|
||||||
|
required this.image,
|
||||||
|
required this.name,
|
||||||
|
required this.partNumber,
|
||||||
|
required this.category,
|
||||||
|
required this.subcategory,
|
||||||
|
required this.price,
|
||||||
|
required this.inventory,
|
||||||
|
required this.offerStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ class Turn14StatusEntity {
|
|||||||
final String? clientId;
|
final String? clientId;
|
||||||
final String? clientSecret;
|
final String? clientSecret;
|
||||||
final String? accessToken;
|
final String? accessToken;
|
||||||
final String? expiresIn;
|
final int? expiresIn;
|
||||||
|
|
||||||
Turn14StatusEntity({
|
Turn14StatusEntity({
|
||||||
required this.userId,
|
required this.userId,
|
||||||
|
|||||||
@ -7,7 +7,18 @@ import 'package:autos/presentation/screens/dashboard/dashboard_screen.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
// ✅ ADD THIS IMPORT
|
||||||
|
import 'package:onesignal_flutter/onesignal_flutter.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
// ONESIGNAL SETUP (ADDED SAFELY)
|
||||||
|
OneSignal.Debug.setLogLevel(OSLogLevel.verbose);
|
||||||
|
OneSignal.initialize("271c3931-07ee-46b6-8629-7f0d63f58085");
|
||||||
|
OneSignal.Notifications.requestPermission(false);
|
||||||
|
|
||||||
|
// KEEP YOUR ORIGINAL LOGIC
|
||||||
runApp(const ProviderScope(child: MyApp()));
|
runApp(const ProviderScope(child: MyApp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,20 +41,22 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||||||
Future<void> _restoreSession() async {
|
Future<void> _restoreSession() async {
|
||||||
final userNotifier = ref.read(userProvider.notifier);
|
final userNotifier = ref.read(userProvider.notifier);
|
||||||
|
|
||||||
// Load user from secure storage
|
// ✅ Load user from secure storage (UNCHANGED)
|
||||||
await userNotifier.loadUserFromStorage();
|
await userNotifier.loadUserFromStorage();
|
||||||
|
|
||||||
// Stop loading after user is restored
|
// ✅ Stop loading after user is restored (UNCHANGED)
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Show loading screen until user session is restored
|
// ✅ Loading screen (UNCHANGED)
|
||||||
if (_isLoading) {
|
if (_isLoading) {
|
||||||
return const MaterialApp(
|
return const MaterialApp(
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
home: Scaffold(body: Center(child: CircularProgressIndicator())),
|
home: Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +68,9 @@ class _MyAppState extends ConsumerState<MyApp> {
|
|||||||
title: "Autos",
|
title: "Autos",
|
||||||
theme: AppTheme.lightTheme,
|
theme: AppTheme.lightTheme,
|
||||||
|
|
||||||
// Dynamic home based on session
|
// ✅ Dynamic home based on session (UNCHANGED)
|
||||||
home: user != null ? const DashboardScreen() : const LoginScreen(),
|
home: user != null ? const DashboardScreen() : const LoginScreen(),
|
||||||
|
|
||||||
navigatorKey: NavigationService.navigatorKey,
|
navigatorKey: NavigationService.navigatorKey,
|
||||||
onGenerateRoute: AppRouter.generateRoute,
|
onGenerateRoute: AppRouter.generateRoute,
|
||||||
);
|
);
|
||||||
|
|||||||
84
lib/presentation/providers/imports_provider.dart
Normal file
84
lib/presentation/providers/imports_provider.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'package:autos/data/repositories/imports_repository_impl.dart';
|
||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
import 'package:autos/presentation/providers/user_provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
|
|
||||||
|
/// ✅ Repository Provider
|
||||||
|
final importsRepositoryProvider = Provider<ImportsRepository>(
|
||||||
|
(ref) => ImportsRepositoryImpl(ref.read(apiServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// ✅ Main Imports Provider
|
||||||
|
final importsProvider = StateNotifierProvider<
|
||||||
|
ImportsNotifier, AsyncValue<List<ImportProductEntity>>>(
|
||||||
|
(ref) => ImportsNotifier(ref.read(importsRepositoryProvider)),
|
||||||
|
);
|
||||||
|
class ImportsNotifier
|
||||||
|
extends StateNotifier<AsyncValue<List<ImportProductEntity>>> {
|
||||||
|
final ImportsRepository _repo;
|
||||||
|
|
||||||
|
ImportsNotifier(this._repo) : super(const AsyncLoading());
|
||||||
|
|
||||||
|
/// ✅ STATE
|
||||||
|
int _page = 1;
|
||||||
|
final int _pageSize = 48;
|
||||||
|
|
||||||
|
String _category = "All";
|
||||||
|
String _subcategory = "All";
|
||||||
|
|
||||||
|
int get currentPage => _page;
|
||||||
|
|
||||||
|
/// ✅ LOAD PRODUCTS FROM API
|
||||||
|
Future<void> loadProducts({bool resetPage = false}) async {
|
||||||
|
try {
|
||||||
|
if (resetPage) _page = 1;
|
||||||
|
|
||||||
|
state = const AsyncLoading();
|
||||||
|
|
||||||
|
debugPrint("📡 Fetching products");
|
||||||
|
debugPrint("➡ Page: $_page");
|
||||||
|
debugPrint("➡ Category: $_category");
|
||||||
|
debugPrint("➡ SubCategory: $_subcategory");
|
||||||
|
|
||||||
|
final products = await _repo.getUserProducts(
|
||||||
|
page: _page,
|
||||||
|
pageSize: _pageSize,
|
||||||
|
category: _category == "All" ? null : _category,
|
||||||
|
subcategory: _subcategory == "All" ? null : _subcategory,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = AsyncData(products);
|
||||||
|
} catch (e, st) {
|
||||||
|
state = AsyncError(e, st);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ CATEGORY CHANGE (RESETS SUBCATEGORY AUTOMATICALLY)
|
||||||
|
void changeCategory(String value) {
|
||||||
|
_category = value;
|
||||||
|
_subcategory = "All"; // ✅ RESET HERE
|
||||||
|
loadProducts(resetPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SUBCATEGORY CHANGE
|
||||||
|
void changeSubcategory(String value) {
|
||||||
|
_subcategory = value;
|
||||||
|
loadProducts(resetPage: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ NEXT PAGE
|
||||||
|
void nextPage() {
|
||||||
|
_page++;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ PREVIOUS PAGE
|
||||||
|
void prevPage() {
|
||||||
|
if (_page > 1) {
|
||||||
|
_page--;
|
||||||
|
loadProducts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -33,6 +33,7 @@ class Turn14Notifier extends StateNotifier<AsyncValue<Turn14Entity?>> {
|
|||||||
|
|
||||||
Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
|
Turn14Notifier(this.repository) : super(const AsyncValue.data(null));
|
||||||
|
|
||||||
|
|
||||||
/// Save Turn14 credentials
|
/// Save Turn14 credentials
|
||||||
Future<void> saveCredentials({
|
Future<void> saveCredentials({
|
||||||
required String userId,
|
required String userId,
|
||||||
|
|||||||
@ -4,20 +4,19 @@ import 'package:autos/data/models/user_model.dart';
|
|||||||
import 'package:autos/data/repositories/user_repository_impl.dart';
|
import 'package:autos/data/repositories/user_repository_impl.dart';
|
||||||
import 'package:autos/data/sources/remote/api_service.dart';
|
import 'package:autos/data/sources/remote/api_service.dart';
|
||||||
import 'package:autos/domain/entities/user.dart';
|
import 'package:autos/domain/entities/user.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_riverpod/legacy.dart';
|
import 'package:flutter_riverpod/legacy.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
|
||||||
/// ✅ Provide a single ApiService instance across the app
|
/// Provide a single ApiService instance across the app
|
||||||
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
final apiServiceProvider = Provider<ApiService>((ref) => ApiService());
|
||||||
|
|
||||||
/// ✅ Provide repository that depends on ApiService
|
/// Provide repository that depends on ApiService
|
||||||
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
final userRepositoryProvider = Provider<UserRepositoryImpl>(
|
||||||
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
(ref) => UserRepositoryImpl(ref.read(apiServiceProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// ✅ ✅ ✅ SINGLE GLOBAL USER PROVIDER (ONLY ONE YOU SHOULD USE)
|
/// SINGLE GLOBAL USER PROVIDER
|
||||||
final userProvider =
|
final userProvider =
|
||||||
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
|
||||||
final repo = ref.read(userRepositoryProvider);
|
final repo = ref.read(userRepositoryProvider);
|
||||||
@ -38,7 +37,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
UserNotifier(this.ref, this.repository)
|
UserNotifier(this.ref, this.repository)
|
||||||
: super(const AsyncValue.data(null));
|
: super(const AsyncValue.data(null));
|
||||||
|
|
||||||
// ✅ ✅ AUTO LOGIN ON APP START
|
// AUTO LOGIN ON APP START
|
||||||
Future<void> loadUserFromStorage() async {
|
Future<void> loadUserFromStorage() async {
|
||||||
final jsonString = await _storage.read(key: _userKey);
|
final jsonString = await _storage.read(key: _userKey);
|
||||||
|
|
||||||
@ -53,7 +52,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
state = AsyncValue.data(user);
|
state = AsyncValue.data(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ✅ LOGIN FLOW (FULLY SYNCHRONIZED)
|
// LOGIN FLOW (FULLY SYNCHRONIZED)
|
||||||
Future<void> login(String email, String password) async {
|
Future<void> login(String email, String password) async {
|
||||||
lastAction = AuthAction.login;
|
lastAction = AuthAction.login;
|
||||||
|
|
||||||
@ -66,17 +65,17 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
// 2️⃣ Fetch FULL user details
|
// 2️⃣ Fetch FULL user details
|
||||||
final fullUser = await repository.getUserDetails(partialUser.id);
|
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());
|
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);
|
state = AsyncValue.data(fullUser);
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
state = AsyncValue.error(e, st);
|
state = AsyncValue.error(e, st);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ✅ SIGNUP
|
// SIGNUP
|
||||||
Future<void> signup(
|
Future<void> signup(
|
||||||
String name,
|
String name,
|
||||||
String email,
|
String email,
|
||||||
@ -114,7 +113,7 @@ class UserNotifier extends StateNotifier<AsyncValue<User?>> {
|
|||||||
state = const AsyncValue.data(null);
|
state = const AsyncValue.data(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ ✅ PASSWORD RESET
|
// PASSWORD RESET
|
||||||
Future<void> sendPasswordResetLink(String email) async {
|
Future<void> sendPasswordResetLink(String email) async {
|
||||||
try {
|
try {
|
||||||
state = const AsyncValue.loading();
|
state = const AsyncValue.loading();
|
||||||
|
|||||||
@ -186,6 +186,7 @@ class _InfoCardState extends State<InfoCard> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 0.5),
|
||||||
margin: const EdgeInsets.only(bottom: 18),
|
margin: const EdgeInsets.only(bottom: 18),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@ -281,6 +282,7 @@ class UserDetailsCard extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
shadowColor: Colors.black.withValues(alpha: 0.5),
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
margin: const EdgeInsets.only(bottom: 18),
|
margin: const EdgeInsets.only(bottom: 18),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
|||||||
245
lib/presentation/screens/imports/imports_header.dart
Normal file
245
lib/presentation/screens/imports/imports_header.dart
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ImportsHeader extends StatefulWidget {
|
||||||
|
final int visibleCount;
|
||||||
|
final int pageSize;
|
||||||
|
final int totalCount;
|
||||||
|
final int selectedCount;
|
||||||
|
|
||||||
|
final Function(String) onSearch;
|
||||||
|
final Function(bool) onStockToggle;
|
||||||
|
final Function(String) onCategoryChange;
|
||||||
|
final Function(String) onSubcategoryChange;
|
||||||
|
final Function(String) onOfferStatusChange;
|
||||||
|
final Function(double) onPriceChange;
|
||||||
|
|
||||||
|
const ImportsHeader({
|
||||||
|
super.key,
|
||||||
|
required this.visibleCount,
|
||||||
|
required this.pageSize,
|
||||||
|
required this.totalCount,
|
||||||
|
required this.selectedCount,
|
||||||
|
required this.onSearch,
|
||||||
|
required this.onStockToggle,
|
||||||
|
required this.onCategoryChange,
|
||||||
|
required this.onSubcategoryChange,
|
||||||
|
required this.onOfferStatusChange,
|
||||||
|
required this.onPriceChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ImportsHeader> createState() => _ImportsHeaderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportsHeaderState extends State<ImportsHeader> {
|
||||||
|
String category = "All";
|
||||||
|
String subcategory = "All";
|
||||||
|
String offerStatus = "All";
|
||||||
|
bool inStockOnly = false;
|
||||||
|
double priceFloor = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool isMobile = MediaQuery.of(context).size.width < 900;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
/// ✅ TOP SECTION (OVERFLOW SAFE)
|
||||||
|
isMobile ? _mobileTopBar() : _desktopTopBar(),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
/// ✅ FILTER ROW
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
/// ✅ CATEGORY (RESETS SUBCATEGORY)
|
||||||
|
_dropdown(
|
||||||
|
label: "Category",
|
||||||
|
value: category,
|
||||||
|
items: const ["All", "Floor Mats", "Wheels"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() {
|
||||||
|
category = value;
|
||||||
|
subcategory = "All"; // ✅ RESET
|
||||||
|
});
|
||||||
|
|
||||||
|
widget.onCategoryChange(value);
|
||||||
|
widget.onSubcategoryChange("All"); // ✅ FORCE API RESET
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ SUBCATEGORY
|
||||||
|
_dropdown(
|
||||||
|
label: "Subcategory",
|
||||||
|
value: subcategory,
|
||||||
|
items: const ["All", "Wheels - Cast", "Wheels - Forged"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() => subcategory = value);
|
||||||
|
widget.onSubcategoryChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ OFFER STATUS
|
||||||
|
_dropdown(
|
||||||
|
label: "Offer Status",
|
||||||
|
value: offerStatus,
|
||||||
|
items: const ["All", "Published", "Draft"],
|
||||||
|
onChanged: (v) {
|
||||||
|
final value = v ?? "All";
|
||||||
|
setState(() => offerStatus = value);
|
||||||
|
widget.onOfferStatusChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ PRICE SLIDER
|
||||||
|
SizedBox(
|
||||||
|
width: 260,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text("Price Floor"),
|
||||||
|
Slider(
|
||||||
|
min: 0,
|
||||||
|
max: 1000,
|
||||||
|
value: priceFloor,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => priceFloor = value);
|
||||||
|
widget.onPriceChange(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Text("Price ≥ \$${priceFloor.toInt()}"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ DESKTOP TOP BAR
|
||||||
|
Widget _desktopTopBar() {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Expanded(child: _titleSection()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Flexible(child: _searchBox()),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 12,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [_stockToggle(), _actionButton()],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ MOBILE TOP BAR
|
||||||
|
Widget _mobileTopBar() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_titleSection(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_searchBox(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(spacing: 12, children: [_stockToggle(), _actionButton()]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ TITLE
|
||||||
|
Widget _titleSection() {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Imports", style: AppTypo.h3),
|
||||||
|
Text(
|
||||||
|
"${widget.visibleCount} of ${widget.totalCount} products",
|
||||||
|
style: AppTypo.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ SEARCH BOX
|
||||||
|
Widget _searchBox() {
|
||||||
|
return SizedBox(
|
||||||
|
height: 44,
|
||||||
|
child: TextField(
|
||||||
|
onChanged: widget.onSearch,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: "Search imports...",
|
||||||
|
prefixIcon: Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ STOCK TOGGLE
|
||||||
|
Widget _stockToggle() {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Switch(
|
||||||
|
value: inStockOnly,
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() => inStockOnly = v);
|
||||||
|
widget.onStockToggle(v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
const Text("In Stock Only"),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ ACTION BUTTON
|
||||||
|
Widget _actionButton() {
|
||||||
|
return ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(24.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text("Bulk Action"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ REUSABLE DROPDOWN
|
||||||
|
Widget _dropdown({
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required List<String> items,
|
||||||
|
required Function(String?) onChanged,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(label),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
onChanged: onChanged,
|
||||||
|
items: items
|
||||||
|
.map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
||||||
|
.toList(),
|
||||||
|
decoration: const InputDecoration(border: OutlineInputBorder()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
315
lib/presentation/screens/imports/imports_screen.dart
Normal file
315
lib/presentation/screens/imports/imports_screen.dart
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import 'package:autos/core/theme/app_typography.dart';
|
||||||
|
import 'package:autos/core/widgets/hamburger_button.dart';
|
||||||
|
import 'package:autos/core/widgets/side_menu.dart';
|
||||||
|
import 'package:autos/domain/entities/import_product.dart';
|
||||||
|
import 'package:autos/presentation/providers/imports_provider.dart';
|
||||||
|
import 'package:autos/presentation/screens/imports/imports_header.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class ImportsScreen extends ConsumerStatefulWidget {
|
||||||
|
const ImportsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<ImportsScreen> createState() => _ImportsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImportsScreenState extends ConsumerState<ImportsScreen> {
|
||||||
|
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
|
||||||
|
String selected = 'imports';
|
||||||
|
|
||||||
|
/// ✅ Selection & Filters
|
||||||
|
Set<String> selectedProducts = {};
|
||||||
|
bool inStockOnly = false;
|
||||||
|
String searchQuery = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
/// ✅ Load API data on screen open
|
||||||
|
Future.microtask(() {
|
||||||
|
ref.read(importsProvider.notifier).loadProducts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ✅ FILTER LOGIC
|
||||||
|
List<ImportProductEntity> filterProducts(List<ImportProductEntity> products) {
|
||||||
|
return products.where((product) {
|
||||||
|
final matchesSearch = product.name.toLowerCase().contains(
|
||||||
|
searchQuery.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final matchesStock =
|
||||||
|
!inStockOnly || product.inventory > 0; // ✅ REAL STOCK CHECK
|
||||||
|
|
||||||
|
return matchesSearch && matchesStock;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final double topPadding = MediaQuery.of(context).padding.top + 16;
|
||||||
|
final productsAsync = ref.watch(importsProvider);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
key: _scaffoldKey,
|
||||||
|
drawer: SideMenu(
|
||||||
|
selected: selected,
|
||||||
|
onItemSelected: (key) {
|
||||||
|
setState(() => selected = key);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFFF6FDFF),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
/// ✅ TITLE
|
||||||
|
Positioned(
|
||||||
|
top: topPadding,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Imports",
|
||||||
|
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ MAIN CONTENT
|
||||||
|
SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.fromLTRB(16, topPadding + 70, 16, 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ImportsHeader(
|
||||||
|
visibleCount: 47,
|
||||||
|
pageSize: 48,
|
||||||
|
totalCount: 373,
|
||||||
|
selectedCount: selectedProducts.length,
|
||||||
|
|
||||||
|
onSearch: (value) {
|
||||||
|
debugPrint("Search: $value");
|
||||||
|
},
|
||||||
|
|
||||||
|
onStockToggle: (value) {
|
||||||
|
debugPrint("Stock Only: $value");
|
||||||
|
},
|
||||||
|
|
||||||
|
onCategoryChange: (value) {
|
||||||
|
debugPrint("Category: $value");
|
||||||
|
},
|
||||||
|
|
||||||
|
onSubcategoryChange: (value) {
|
||||||
|
debugPrint("Subcategory: $value");
|
||||||
|
},
|
||||||
|
|
||||||
|
onOfferStatusChange: (value) {
|
||||||
|
debugPrint("Offer Status: $value");
|
||||||
|
},
|
||||||
|
|
||||||
|
onPriceChange: (value) {
|
||||||
|
debugPrint("Price Floor: $value");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
/// ✅ PRODUCT LIST FROM API
|
||||||
|
productsAsync.when(
|
||||||
|
loading: () => const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 40),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
error: (e, _) => Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Text("❌ Error: $e"),
|
||||||
|
),
|
||||||
|
data: (products) {
|
||||||
|
final filtered = filterProducts(products);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: filtered.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final product = filtered[index];
|
||||||
|
final isSelected = selectedProducts.contains(
|
||||||
|
product.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ImportProductCard(
|
||||||
|
product: product,
|
||||||
|
selected: isSelected,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (isSelected) {
|
||||||
|
selectedProducts.remove(product.id);
|
||||||
|
} else {
|
||||||
|
selectedProducts.add(product.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
/// ✅ SUBMIT BUTTON
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: selectedProducts.isEmpty
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Submitted ${selectedProducts.length} products',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Submit (${selectedProducts.length}) selected items',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ HAMBURGER BUTTON
|
||||||
|
HamburgerButton(scaffoldKey: _scaffoldKey),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImportProductCard extends StatelessWidget {
|
||||||
|
final ImportProductEntity product;
|
||||||
|
final bool selected;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const ImportProductCard({
|
||||||
|
super.key,
|
||||||
|
required this.product,
|
||||||
|
required this.selected,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
elevation: 2,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(14),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
/// ✅ TOP ROW
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(value: selected, onChanged: (_) => onTap()),
|
||||||
|
const Text("Select"),
|
||||||
|
const Spacer(),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue.withOpacity(.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text("Will list: ${product.inventory}"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
/// ✅ IMAGE
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
product.image,
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
errorBuilder: (_, __, ___) =>
|
||||||
|
const Icon(Icons.image_not_supported, size: 80),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
/// ✅ TITLE
|
||||||
|
Text(
|
||||||
|
product.name,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
/// ✅ META INFO
|
||||||
|
Text("Part #: ${product.partNumber}"),
|
||||||
|
Text("${product.category} > ${product.subcategory}"),
|
||||||
|
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
/// ✅ PRICE + INVENTORY
|
||||||
|
Text(
|
||||||
|
"Price: \$${product.price}",
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text("Inventory: ${product.inventory}"),
|
||||||
|
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
|
/// ✅ DESCRIPTION PREVIEW
|
||||||
|
Text(product.name, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||||
|
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
/// ✅ STATUS TAGS
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_statusChip("published", product.offerStatus == "published"),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_statusChip("Listing", true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statusChip(String label, bool active) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: active ? Colors.green.withOpacity(.15) : Colors.grey[200],
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: active ? Colors.green : Colors.grey[600],
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -93,7 +93,7 @@ class _ProductsScreenState extends ConsumerState<ProductsScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
/// ✅ Main Content
|
/// Main Content
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
padding: EdgeInsets.fromLTRB(16, topPadding + 55, 16, 20),
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:autos/core/theme/app_theme.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CreateLocationScreen extends StatefulWidget {
|
class CreateLocationScreen extends StatefulWidget {
|
||||||
@ -16,7 +17,9 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
final TextEditingController stateCtrl = TextEditingController();
|
final TextEditingController stateCtrl = TextEditingController();
|
||||||
final TextEditingController postalCode = TextEditingController();
|
final TextEditingController postalCode = TextEditingController();
|
||||||
final TextEditingController country = TextEditingController();
|
final TextEditingController country = TextEditingController();
|
||||||
final TextEditingController timeZone = TextEditingController(text: "America/New_York");
|
final TextEditingController timeZone = TextEditingController(
|
||||||
|
text: "America/New_York",
|
||||||
|
);
|
||||||
TimeOfDay openTime = const TimeOfDay(hour: 9, minute: 0);
|
TimeOfDay openTime = const TimeOfDay(hour: 9, minute: 0);
|
||||||
TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0);
|
TimeOfDay closeTime = const TimeOfDay(hour: 18, minute: 0);
|
||||||
|
|
||||||
@ -28,8 +31,10 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
|
|
||||||
if (picked != null) {
|
if (picked != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (isOpen) openTime = picked;
|
if (isOpen)
|
||||||
else closeTime = picked;
|
openTime = picked;
|
||||||
|
else
|
||||||
|
closeTime = picked;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,8 +44,11 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFEFFAFF),
|
backgroundColor: const Color(0xFFEFFAFF),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text("Create New Location"),
|
title: const Text(
|
||||||
backgroundColor: Colors.white,
|
"Create New Location",
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
backgroundColor: AppTheme.primary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
),
|
),
|
||||||
@ -57,7 +65,7 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
color: Colors.black12,
|
color: Colors.black12,
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -77,9 +85,17 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: _input("Postal Code *", "Postal Code", postalCode)),
|
Expanded(
|
||||||
|
child: _input("Postal Code *", "Postal Code", postalCode),
|
||||||
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(child: _input("Country *", "Country Code (e.g. US)", country)),
|
Expanded(
|
||||||
|
child: _input(
|
||||||
|
"Country *",
|
||||||
|
"Country Code (e.g. US)",
|
||||||
|
country,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
@ -159,8 +175,10 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
label,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
@ -190,8 +208,10 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(
|
||||||
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14)),
|
label,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18),
|
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 18),
|
||||||
@ -201,13 +221,15 @@ class _CreateLocationScreenState extends State<CreateLocationScreen> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Text(time.format(context),
|
Text(
|
||||||
style: const TextStyle(fontSize: 16)),
|
time.format(context),
|
||||||
|
style: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
const Icon(Icons.access_time, size: 20),
|
const Icon(Icons.access_time, size: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -472,6 +472,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
|
onesignal_flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: onesignal_flutter
|
||||||
|
sha256: b5bb43bf496ddb5e3975ba54c6477cc2d1fcd18fb3698f195d2e0bfd376ddafd
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.3.4"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -21,6 +21,7 @@ dependencies:
|
|||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
youtube_player_flutter: ^9.1.3
|
youtube_player_flutter: ^9.1.3
|
||||||
flutter_launcher_icons: ^0.14.4
|
flutter_launcher_icons: ^0.14.4
|
||||||
|
onesignal_flutter: ^5.3.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user