BackendCall Implemented for GET Token and Brands data.

This commit is contained in:
bala 2025-11-27 00:35:58 +05:30
parent 41970033ad
commit 595554d541
12 changed files with 538 additions and 19 deletions

View File

@ -1,5 +1,5 @@
class ApiEndpoints {
static const String baseUrl = 'https://ebay.backend.data4autos.com/api';
static const String baseUrl = 'https://ebay.backend.data4autos.com';
static const Duration connectTimeout = Duration(seconds: 10);
static const Duration receiveTimeout = Duration(seconds: 10);
@ -9,12 +9,18 @@ class ApiEndpoints {
};
///Authentication
static const login = '/auth/login';
static const signup = '/auth/signup';
static const forgotPassword = '/auth/forgot-password';
static const userDetails = '/auth/users/';
static const login = '/api/auth/login';
static const signup = '/api/auth/signup';
static const forgotPassword = '/api/auth/forgot-password';
static const userDetails = '/api/auth/users/';
///GET Token
static const getToken = '/api/auth/turn14/get-access-token';
///Turn14
static const turn14Save = '/auth/turn14/save';
static const turn14Status = '/auth/turn14/status';
static const turn14Save = '/api/auth/turn14/save';
static const turn14Status = '/api/auth/turn14/status';
///Brands
static const String brands = "/v1/brands";
}

View File

@ -2,6 +2,7 @@ import 'package:autos/core/routing/route_paths.dart';
import 'package:autos/presentation/screens/auth/forgot_password_screen.dart';
import 'package:autos/presentation/screens/auth/login_screen.dart';
import 'package:autos/presentation/screens/auth/sign_up_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/ebay/ebay_screen.dart';
import 'package:autos/presentation/screens/store/create_location_screen.dart';
@ -57,6 +58,9 @@ class AppRouter {
case AppRoutePaths.createStoreLocation:
return slideRoute(CreateLocationScreen());
case AppRoutePaths.brands:
return slideRoute(BrandsScreen());
default:
return _defaultFallback(settings);
}

View File

@ -7,4 +7,5 @@ class AppRoutePaths {
static const ebay = '/ebay';
static const store = '/store';
static const createStoreLocation = '/createStoreLocation';
static const brands = '/brands';
}

View File

@ -64,7 +64,7 @@ class SideMenu extends ConsumerWidget {
// --- MANAGE ---
_sectionHeader("MANAGE"),
_menuItem(context, "🏷️", "Brands", 'brands'),
_menuItem(context, "🏷️", "Brands", AppRoutePaths.brands),
_menuItem(context, "📦", "Products", 'products'),
_menuItem(context, "⬇️", "Imports", 'imports'),

View File

@ -0,0 +1,107 @@
import 'package:autos/domain/entities/brands.dart';
class Brand {
final String id;
final String name;
final String logo;
final bool dropship;
final List<PriceGroup> pricegroups;
Brand({
required this.id,
required this.name,
required this.logo,
required this.dropship,
required this.pricegroups,
});
factory Brand.fromJson(Map<String, dynamic> json) {
return Brand(
id: json['id'].toString(),
name: json['name'] ?? '',
logo: json['logo'] ?? '',
dropship: json['dropship'] ?? false,
pricegroups: (json['pricegroups'] as List<dynamic>?)
?.map((e) => PriceGroup.fromJson(e))
.toList() ??
[],
);
}
/// 🔥 Model Entity
BrandEntity toEntity() {
return BrandEntity(
id: id,
name: name,
logo: logo,
dropship: dropship,
pricegroups: pricegroups.map((e) => e.toEntity()).toList(),
);
}
}
class PriceGroup {
final String pricegroupId;
final String pricegroupName;
final String pricegroupPrefix;
final List<dynamic> locationRules;
final List<PurchaseRestriction> purchaseRestrictions;
PriceGroup({
required this.pricegroupId,
required this.pricegroupName,
required this.pricegroupPrefix,
required this.locationRules,
required this.purchaseRestrictions,
});
factory PriceGroup.fromJson(Map<String, dynamic> json) {
return PriceGroup(
pricegroupId: json['pricegroup_id'].toString(),
pricegroupName: json['pricegroup_name'] ?? '',
pricegroupPrefix: json['pricegroup_prefix'] ?? '',
locationRules: json['location_rules'] ?? [],
purchaseRestrictions: (json['purchase_restrictions'] as List<dynamic>?)
?.map((e) => PurchaseRestriction.fromJson(e))
.toList() ??
[],
);
}
/// 🔥 Model Entity
PriceGroupEntity toEntity() {
return PriceGroupEntity(
pricegroupId: pricegroupId,
pricegroupName: pricegroupName,
pricegroupPrefix: pricegroupPrefix,
locationRules: locationRules,
purchaseRestrictions:
purchaseRestrictions.map((e) => e.toEntity()).toList(),
);
}
}
class PurchaseRestriction {
final String program;
final String yourStatus;
PurchaseRestriction({
required this.program,
required this.yourStatus,
});
factory PurchaseRestriction.fromJson(Map<String, dynamic> json) {
return PurchaseRestriction(
program: json['program'] ?? '',
yourStatus: json['your_status'] ?? '',
);
}
/// 🔥 Model Entity
PurchaseRestrictionEntity toEntity() {
return PurchaseRestrictionEntity(
program: program,
yourStatus: yourStatus,
);
}
}

View File

@ -0,0 +1,91 @@
import 'dart:convert';
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/models/brands_model.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/brands.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 BrandsRepository {
Future<List<BrandEntity>> getBrands();
}
class BrandsRepositoryImpl implements BrandsRepository {
final ApiService _apiService;
final _storage = const FlutterSecureStorage();
static const _userKey = 'logged_in_user';
BrandsRepositoryImpl(this._apiService);
/// ALWAYS CALL TOKEN API (FORCE REFRESH)
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") {
// New token returned
token = data["access_token"];
await _storage.write(key: 'turn14_token', value: token);
} else if (data["code"] == "TOKEN_VALID") {
// Token still valid, read from storage
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;
}
/// FETCH BRANDS USING FRESH TOKEN EVERY TIME
@override
Future<List<BrandEntity>> getBrands() async {
try {
final token = await getTurn14AccessToken();
final response = await _apiService.get(
'/v1/brands',
overrideBaseUrl: 'https://turn14.data4autos.com',
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
},
);
if (response.statusCode != 200) {
throw Exception("Server error: ${response.statusCode}");
}
final data = response.data;
if (data is! Map || data["data"] == null) {
throw Exception("Invalid API response");
}
final list = data["data"] as List;
return list.map((json) => Brand.fromJson(json).toEntity()).toList();
} on DioException catch (e) {
throw Exception("Network error: ${e.response?.data ?? e.message}");
} catch (e) {
throw Exception("Unexpected error: $e");
}
}
}

View File

@ -0,0 +1,57 @@
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class TokenRepository {
final ApiService apiService;
final _storage = const FlutterSecureStorage();
TokenRepository(this.apiService);
static const _tokenKey = "turn14_token";
static const _tokenTimeKey = "turn14_token_time";
/// Get Valid Token (Auto Refresh If Expired)
Future<String> getValidToken(String userId) async {
final token = await _storage.read(key: _tokenKey);
final time = await _storage.read(key: _tokenTimeKey);
if (token != null && time != null) {
final savedTime = DateTime.parse(time);
final diff = DateTime.now().difference(savedTime);
/// Token Valid for 50 minutes
if (diff.inMinutes < 50) {
return token;
}
}
/// 🔄 Token Expired Refresh
return await _refreshToken(userId);
}
/// 🔄 Refresh Token API
Future<String> _refreshToken(String userId) async {
final response = await apiService.post(
ApiEndpoints.getToken,
{"userid": userId},
);
final data = response.data;
if (data["code"] != "TOKEN_UPDATED") {
throw Exception(data["message"]);
}
final token = data["access_token"];
/// Save New Token + Timestamp
await _storage.write(key: _tokenKey, value: token);
await _storage.write(
key: _tokenTimeKey,
value: DateTime.now().toIso8601String(),
);
return token;
}
}

View File

@ -1,3 +1,4 @@
// lib/data/sources/remote/api_service.dart
import 'package:autos/core/constants/api_endpoints.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
@ -6,14 +7,14 @@ class ApiService {
final Dio _dio;
ApiService({String? baseUrl})
: _dio = Dio(
BaseOptions(
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
connectTimeout: ApiEndpoints.connectTimeout,
receiveTimeout: ApiEndpoints.receiveTimeout,
headers: ApiEndpoints.defaultHeaders,
),
) {
: _dio = Dio(
BaseOptions(
baseUrl: baseUrl ?? ApiEndpoints.baseUrl,
connectTimeout: ApiEndpoints.connectTimeout,
receiveTimeout: ApiEndpoints.receiveTimeout,
headers: ApiEndpoints.defaultHeaders,
),
) {
// Add logging interceptor in debug mode
if (kDebugMode) {
_dio.interceptors.add(
@ -22,6 +23,7 @@ class ApiService {
}
}
/// POST request
Future<Response> post(
String endpoint,
Map<String, dynamic> data, {
@ -30,7 +32,21 @@ class ApiService {
return await _dio.post(endpoint, data: data, options: options);
}
Future<Response> get(String endpoint, {Map<String, dynamic>? params}) async {
return await _dio.get(endpoint, queryParameters: params);
}
/// GET request with optional overrideBaseUrl and headers
Future<Response> get(
String endpoint, {
Map<String, dynamic>? params,
Map<String, dynamic>? headers, // NOW SUPPORTED
String? overrideBaseUrl,
}) async {
final String requestUrl =
overrideBaseUrl != null ? overrideBaseUrl + endpoint : endpoint;
return await _dio.get(
requestUrl,
queryParameters: params,
options: headers != null ? Options(headers: headers) : null,
);
}
}

View File

@ -0,0 +1,41 @@
class BrandEntity {
final String id;
final String name;
final String logo;
final bool dropship;
final List<PriceGroupEntity> pricegroups;
BrandEntity({
required this.id,
required this.name,
required this.logo,
required this.dropship,
required this.pricegroups,
});
}
class PriceGroupEntity {
final String pricegroupId;
final String pricegroupName;
final String pricegroupPrefix;
final List<dynamic> locationRules;
final List<PurchaseRestrictionEntity> purchaseRestrictions;
PriceGroupEntity({
required this.pricegroupId,
required this.pricegroupName,
required this.pricegroupPrefix,
required this.locationRules,
required this.purchaseRestrictions,
});
}
class PurchaseRestrictionEntity {
final String program;
final String yourStatus;
PurchaseRestrictionEntity({
required this.program,
required this.yourStatus,
});
}

View File

@ -0,0 +1,55 @@
import 'package:autos/data/repositories/brands_repository_impl.dart';
import 'package:autos/data/sources/remote/api_service.dart';
import 'package:autos/domain/entities/brands.dart';
import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart';
/// API Service Provider
final brandsApiServiceProvider = Provider<ApiService>((ref) => ApiService());
/// Repository Provider
final brandsRepositoryProvider = Provider<BrandsRepositoryImpl>(
(ref) => BrandsRepositoryImpl(ref.read(brandsApiServiceProvider)),
);
/// Brands Notifier
class BrandsNotifier extends StateNotifier<AsyncValue<List<BrandEntity>>> {
final BrandsRepositoryImpl repository;
final Ref ref;
BrandsNotifier(this.repository, this.ref) : super(const AsyncValue.loading());
/// Fetch brands every time page opens
Future<void> fetchBrands() async {
state = const AsyncValue.loading();
try {
// 1 Get user safely
final userAsync = ref.read(userDetailsProvider);
final user = userAsync.value;
if (user == null) throw Exception("User not logged in");
// 2 Always fetch fresh token
final token = await repository.getTurn14AccessToken();
// 3 Fetch brands
final brands = await repository.getBrands();
state = AsyncValue.data(brands);
} catch (e, st) {
state = AsyncValue.error(e, st);
debugPrint("Brands fetch error: $e");
}
}
}
/// Brands State Provider
final brandsProvider =
StateNotifierProvider<BrandsNotifier, AsyncValue<List<BrandEntity>>>(
(ref) {
final repo = ref.read(brandsRepositoryProvider);
return BrandsNotifier(repo, ref);
},
);

View File

@ -0,0 +1,7 @@
import 'package:autos/presentation/providers/user_provider.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:autos/data/repositories/token_repository.dart';
final tokenRepositoryProvider = Provider<TokenRepository>(
(ref) => TokenRepository(ref.read(apiServiceProvider)),
);

View File

@ -0,0 +1,134 @@
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/brands.dart';
import 'package:autos/presentation/providers/brand_provider.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BrandsScreen extends ConsumerStatefulWidget {
const BrandsScreen({super.key});
@override
ConsumerState<BrandsScreen> createState() => _BrandsScreenState();
}
class _BrandsScreenState extends ConsumerState<BrandsScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
String selected = 'brands';
@override
void initState() {
super.initState();
/// Fetch brands every time page opens
Future.microtask(() {
ref.read(brandsProvider.notifier).fetchBrands();
});
}
@override
Widget build(BuildContext context) {
final topPadding = MediaQuery.of(context).padding.top + 16;
final brandsState = ref.watch(brandsProvider);
return Scaffold(
key: _scaffoldKey,
drawer: SideMenu(
selected: selected,
onItemSelected: (key) => setState(() => selected = key),
),
body: Stack(
children: [
Positioned(
top: topPadding,
left: 0,
right: 0,
child: Center(
child: Text(
"Brands",
style: AppTypo.h2.copyWith(fontWeight: FontWeight.w700),
),
),
),
HamburgerButton(scaffoldKey: _scaffoldKey),
Positioned.fill(
top: topPadding + 60,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: brandsState.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, st) => Center(
child: Text(
err.toString(),
style: const TextStyle(color: Colors.red),
),
),
data: (brands) {
if (brands.isEmpty) {
return const Center(child: Text("No brands available."));
}
return GridView.builder(
padding: const EdgeInsets.only(bottom: 20),
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1,
),
itemCount: brands.length,
itemBuilder: (context, index) {
final BrandEntity brand = brands[index];
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 6,
offset: Offset(0, 3),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
brand.logo,
height: 60,
width: 60,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) =>
const Icon(Icons.image, size: 40),
),
),
const SizedBox(height: 12),
Text(
brand.name,
textAlign: TextAlign.center,
style: AppTypo.h3.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
);
},
),
),
),
],
),
);
}
}