refresh token api integreted and isLogin created in local storages

This commit is contained in:
mystery012728
2026-01-27 18:47:15 +05:30
parent f5782f6da1
commit 1cb344738e
13 changed files with 619 additions and 219 deletions

View File

@@ -8,6 +8,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../common_packages/common_app_texts.dart';
import '../../localPreference/local_preference.dart';
import '../../postcard/widgets/purchase_details_bottom_sheet.dart';
import '../widget/pass_purchase_details_bottomsheet.dart';
class CheckoutView extends StatelessWidget {
const CheckoutView({super.key});
@@ -345,22 +348,35 @@ class CheckoutView extends StatelessWidget {
],
),
const Spacer(),
CustomFilledButton(
onTap: () {
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
FutureBuilder<bool>(
future: LocalPreference.getLogin(),
builder: (context, snapshot) {
final isLoggedIn = snapshot.data ?? false;
return CustomFilledButton(
onTap: () {
if (isLoggedIn) {
// Show purchase details bottom sheet if logged in
PassPurchaseBottomSheet.show(context);
} else {
// Show login bottom sheet if not logged in
showModalBottomSheet(
backgroundColor: Colors.white,
context: context,
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(
top: Radius.circular(12.r),
),
),
builder: (_) => const LoginEmailBottomsheet(),
);
}
},
width: double.infinity,
label: isLoggedIn ? "Checkout" : "Login to Checkout",
);
},
width: double.infinity,
label: "Login to Checkout",
),
SizedBox(height: 25.h),
],

View File

@@ -0,0 +1,238 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class PassPurchaseBottomSheet {
static void show(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.white,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (BuildContext modalContext) {
return _PassPurchaseContent();
},
);
}
static void close(BuildContext context) {
Navigator.of(context).pop();
}
}
class _PassPurchaseContent extends StatefulWidget {
@override
State<_PassPurchaseContent> createState() => _PassPurchaseContentState();
}
class _PassPurchaseContentState extends State<_PassPurchaseContent> {
bool isGift = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
top: 16,
left: 16,
right: 16,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle bar
Container(
width: 45,
height: 5,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 12),
// Title
Text(
"Purchase Details",
style: TextStyle(
fontSize: 16.sp,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 24),
// Option 1: Buy Pass for Myself
GestureDetector(
onTap: () => setState(() => isGift = false),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: !isGift
? Border.all(
color: const Color(0xffF95F62),
width: 1.5,
)
: null,
),
child: Row(
children: [
Radio<bool>(
value: false,
groupValue: isGift,
onChanged: (value) => setState(() => isGift = false),
activeColor: const Color(0xffF95F62),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Buy Pass for Myself",
style: TextStyle(
fontWeight: FontWeight.w600,
color: !isGift
? const Color(0xffF95F62)
: const Color(0xff9E9E9E),
),
),
if (!isGift) ...[
const SizedBox(height: 8),
const Text(
"Frank Adam",
style: TextStyle(
fontWeight: FontWeight.w500,
color: Color(0xff1A1A1A),
),
),
const Text(
"132 My Street, Kingston, NY\n12401",
style: TextStyle(
fontSize: 13,
color: Color(0xff5E5E5E),
),
),
],
],
),
),
if (!isGift)
ElevatedButton(
onPressed: () {
// Handle edit details
PassPurchaseBottomSheet.close(context);
// Navigate to edit details screen
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
"Edit Details",
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
const SizedBox(height: 20),
// Option 2: Gift the Pass
GestureDetector(
onTap: () => setState(() => isGift = true),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: isGift
? Border.all(
color: const Color(0xffF95F62),
width: 1.5,
)
: null,
),
child: Row(
children: [
Radio<bool>(
value: true,
groupValue: isGift,
onChanged: (value) => setState(() => isGift = true),
activeColor: const Color(0xffF95F62),
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Gift the pass",
style: TextStyle(
fontWeight: FontWeight.w600,
color: isGift
? const Color(0xffF95F62)
: const Color(0xff9E9E9E),
),
),
if (isGift)
const SizedBox(height: 4),
if (isGift)
const Text(
"Gift the pass for someone else",
style: TextStyle(
fontSize: 13,
color: Color(0xff9E9E9E),
),
),
],
),
),
],
),
),
),
const SizedBox(height: 15),
// Proceed Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
PassPurchaseBottomSheet.close(context);
// Handle proceed action
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xffF95F62),
padding: EdgeInsets.symmetric(vertical: 16.h),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: Text(
"Proceed",
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 15),
],
),
);
}
}

View File

@@ -1,4 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../localPreference/local_preference.dart';
import '../models/create_account_model.dart';
import '../repository/create_account_repository.dart';
import 'create_account_event.dart';
import 'create_account_state.dart';
@@ -28,6 +30,12 @@ class CreateAccountBloc extends Bloc<CreateAccountEvent, CreateAccountState> {
address2: event.address2,
);
final userModel = UserRegisteredModel.fromJson(response['data'] ?? {});
await LocalPreference.setTokens(
accessToken: userModel.accessToken,
refreshToken: userModel.refreshToken,
refreshTokenMaxAge: userModel.refreshTokenMaxAge,
);
emit(CreateAccountSuccess(
message: response['message'] ?? 'Account created successfully',
userData: response['data'] ?? {},

View File

@@ -173,19 +173,19 @@ class _ChooseYourPassSectionState extends State<ChooseYourPassSection> {
),
),
const SizedBox(height: 16),
// 🔒 STATIC TEXT (NOT REMOVED)
const Text(
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
"• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
"• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
style: TextStyle(
fontSize: 12,
color: Color(0xff5B5F62),
height: 1.5,
),
),
// const SizedBox(height: 16),
//
// // 🔒 STATIC TEXT (NOT REMOVED)
// const Text(
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.\n"
// "• Pellentesque vel nisl posuere, ullamcorper nibh.\n"
// "• Fusce tincidunt interdum ex, in tincidunt libero porttitor vel.",
// style: TextStyle(
// fontSize: 12,
// color: Color(0xff5B5F62),
// height: 1.5,
// ),
// ),
const Spacer(),

View File

@@ -41,12 +41,22 @@ class LocalDatabase {
/// LOGIN TABLE
await db.execute('''
CREATE TABLE login_state (
id INTEGER PRIMARY KEY,
is_login INTEGER NOT NULL
)
''');
CREATE TABLE login_state (
id INTEGER PRIMARY KEY,
is_logged_in INTEGER NOT NULL
)
''');
/// USER TOKENS TABLE
await db.execute('''
CREATE TABLE user_tokens (
id INTEGER PRIMARY KEY,
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
refresh_token_max_age INTEGER NOT NULL
)
''');
},
);
}
}
}

View File

@@ -91,36 +91,22 @@ class LocalPreference {
await updateOnboardingPage(0);
}
static Future<void> initLoginState() async {
final db = await LocalDatabase().database;
final result = await db.query('login_state');
if (result.isEmpty) {
await db.insert(
'login_state',
{
'id': 1,
'is_login': 0, // false by default
},
);
}
}
static Future<void> setIsLogin(bool value) async {
/// Set login state
static Future<void> setLogin(bool value) async {
final db = await LocalDatabase().database;
await db.insert(
'login_state',
{
'id': 1,
'is_login': value ? 1 : 0,
'is_logged_in': value ? 1 : 0,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
static Future<bool> isLogin() async {
/// Get login state
static Future<bool> getLogin() async {
final db = await LocalDatabase().database;
final result = await db.query(
@@ -130,13 +116,72 @@ class LocalPreference {
);
if (result.isNotEmpty) {
return (result.first['is_login'] as int) == 1;
return result.first['is_logged_in'] == 1;
}
return false;
}
static Future<void> logout() async {
await setIsLogin(false);
/// Set user tokens
static Future<void> setTokens({
required String accessToken,
required String refreshToken,
required int refreshTokenMaxAge,
}) async {
final db = await LocalDatabase().database;
await db.insert(
'user_tokens',
{
'id': 1,
'access_token': accessToken,
'refresh_token': refreshToken,
'refresh_token_max_age': refreshTokenMaxAge,
},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
/// Get access token
static Future<String?> getAccessToken() async {
final db = await LocalDatabase().database;
final result = await db.query(
'user_tokens',
where: 'id = ?',
whereArgs: [1],
);
if (result.isNotEmpty) {
return result.first['access_token'] as String?;
}
return null;
}
/// Get refresh token
static Future<String?> getRefreshToken() async {
final db = await LocalDatabase().database;
final result = await db.query(
'user_tokens',
where: 'id = ?',
whereArgs: [1],
);
if (result.isNotEmpty) {
return result.first['refresh_token'] as String?;
}
return null;
}
/// Clear tokens (for logout)
static Future<void> clearTokens() async {
final db = await LocalDatabase().database;
await db.delete(
'user_tokens',
where: 'id = ?',
whereArgs: [1],
);
}
}

View File

@@ -1,6 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:citycards_customer/login/repository/login_repository.dart';
import '../../../create_account/models/create_account_model.dart';
import '../../../localPreference/local_preference.dart';
import 'verify_event.dart';
import 'verify_state.dart';
@@ -26,6 +27,11 @@ class VerifyOtpBloc extends Bloc<VerifyOtpEvent, VerifyOtpState> {
);
final userModel = UserRegisteredModel.fromJson(response);
await LocalPreference.setTokens(
accessToken: userModel.accessToken,
refreshToken: userModel.refreshToken,
refreshTokenMaxAge: userModel.refreshTokenMaxAge,
);
emit(VerifyOtpSuccess(response: userModel));
} catch (e) {
emit(VerifyOtpError(errorMessage: e.toString()));

View File

@@ -133,34 +133,34 @@ class _LoginEmailBottomsheetState extends State<LoginEmailBottomsheet> {
);
},
),
SizedBox(height: 20.h),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(RouteConstants.createAcct);
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: "Already have an account?",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " Sign in",
style: TextStyle(
color: const Color(0xFFF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
// SizedBox(height: 20.h),
// InkWell(
// onTap: () {
// Navigator.of(context).pushNamed(RouteConstants.createAcct);
// },
// child: Text.rich(
// TextSpan(
// children: [
// TextSpan(
// text: "Already have an account?",
// style: TextStyle(
// color: Colors.black.withOpacity(0.6),
// fontSize: 12.sp,
// fontWeight: FontWeight.w400,
// ),
// ),
// TextSpan(
// text: " Sign in",
// style: TextStyle(
// color: const Color(0xFFF95F62),
// fontSize: 12.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// ),
// ),
// ),
SizedBox(height: 15.h),
],
),

View File

@@ -5,6 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_otp_text_field/flutter_otp_text_field.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../core/route_constants.dart';
import '../../localPreference/local_preference.dart';
import '../bloc/verify/verify_bloc.dart';
import '../bloc/verify/verify_event.dart';
import '../bloc/verify/verify_state.dart';
@@ -27,11 +28,12 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
@override
Widget build(BuildContext context) {
return BlocListener<VerifyOtpBloc, VerifyOtpState>(
listener: (context, state) {
listener: (context, state) async {
if (state is VerifyOtpSuccess) {
Navigator.pop(context); // Close the bottom sheet
if (state.response.userExists) {
await LocalPreference.setLogin(true);
// User exists - navigate to home/dashboard
// Navigator.of(context).pushReplacementNamed(RouteConstants.home);
ScaffoldMessenger.of(context).showSnackBar(
@@ -132,31 +134,31 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
debugPrint("OTP entered: $code");
},
),
SizedBox(height: 20.h),
BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
builder: (context, state) {
final isResending = state is ResendOtpLoading;
return InkWell(
onTap: isResending
? null
: () {
context.read<VerifyOtpBloc>().add(
ResendOtpEvent(emailAddress: widget.emailAddress),
);
},
child: Text(
isResending ? "Resending..." : "Resend OTP",
style: TextStyle(
color: isResending
? Colors.grey
: const Color(0xFFF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
);
},
),
// SizedBox(height: 20.h),
// BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
// builder: (context, state) {
// final isResending = state is ResendOtpLoading;
// return InkWell(
// onTap: isResending
// ? null
// : () {
// context.read<VerifyOtpBloc>().add(
// ResendOtpEvent(emailAddress: widget.emailAddress),
// );
// },
// child: Text(
// isResending ? "Resending..." : "Resend OTP",
// style: TextStyle(
// color: isResending
// ? Colors.grey
// : const Color(0xFFF95F62),
// fontSize: 12.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// );
// },
// ),
SizedBox(height: 22.h),
BlocBuilder<VerifyOtpBloc, VerifyOtpState>(
builder: (context, state) {
@@ -187,34 +189,34 @@ class _VerifyOtpBottomsheetState extends State<VerifyOtpBottomsheet> {
);
},
),
SizedBox(height: 20.h),
InkWell(
onTap: () {
Navigator.of(context).pushNamed(RouteConstants.createAcct);
},
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: "Already have an account?",
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
),
),
TextSpan(
text: " Sign in",
style: TextStyle(
color: const Color(0xFFF95F62),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
// SizedBox(height: 20.h),
// InkWell(
// onTap: () {
// Navigator.of(context).pushNamed(RouteConstants.createAcct);
// },
// child: Text.rich(
// TextSpan(
// children: [
// TextSpan(
// text: "Already have an account?",
// style: TextStyle(
// color: Colors.black.withOpacity(0.6),
// fontSize: 12.sp,
// fontWeight: FontWeight.w400,
// ),
// ),
// TextSpan(
// text: " Sign in",
// style: TextStyle(
// color: const Color(0xFFF95F62),
// fontSize: 12.sp,
// fontWeight: FontWeight.w600,
// ),
// ),
// ],
// ),
// ),
// ),
SizedBox(height: 15.h),
],
),

View File

@@ -2,6 +2,8 @@ class ApiUrls {
static const baseUrl = "https://devapi.citycards.betadelivery.com";
static const refreshToken = "$baseUrl/auth/refresh";
static const cityList = "$baseUrl/mobile/city_list";
// static const upcomingCityList = "$baseUrl/mobile/upcoming_cities";
static const searchCityList = "$baseUrl/mobile/city-selection";

View File

@@ -1,10 +1,15 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../localPreference/local_preference.dart';
import '../networkApiServices/api_urls.dart';
class NetworkApiService {
static final NetworkApiService _instance = NetworkApiService._internal();
late Dio _dio;
bool _isRefreshing = false;
final List<void Function()> _retryQueue = [];
factory NetworkApiService() {
return _instance;
}
@@ -20,80 +25,96 @@ class NetworkApiService {
},
),
);
// === RETRY INTERCEPTOR (for timeouts & connection errors) ===
_dio.interceptors.add(InterceptorsWrapper(
onError: (DioException err, handler) async {
final options = err.requestOptions;
const maxRetries = 2; // Total attempts = 1 initial + 2 retry
final currentRetry = options.extra['retry'] as int? ?? 0;
final shouldRetry = currentRetry < maxRetries &&
(err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout );
if (shouldRetry) {
if (kDebugMode) {
print('🔁 Retrying request (${currentRetry + 1}) to ${options.uri}');
}
// Wait before retrying
// await Future.delayed(const Duration(seconds: 1));
options.extra['retry'] = currentRetry + 1;
// Re-execute the request
try {
final response = await _dio.fetch(options);
return handler.resolve(response);
} on DioException catch (e) {
return handler.reject(e);
}
}
// Not retrying → propagate original error
return handler.reject(err);
},
));
// === MAIN INTERCEPTOR (logging, auth, etc.) ===
// ================= RETRY INTERCEPTOR =================
_dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// Add token if available (uncomment when needed)
// String? token = "your_token_here";
// if (token != null) {
// options.headers['Authorization'] = 'Bearer $token';
// }
onError: (err, handler) async {
final options = err.requestOptions;
const maxRetries = 2;
final currentRetry = options.extra['retry'] as int? ?? 0;
if (kDebugMode) {
print('📤 REQUEST[${options.method}] => URL: ${options.uri}');
final shouldRetry = currentRetry < maxRetries &&
(err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.sendTimeout ||
err.type == DioExceptionType.receiveTimeout);
if (shouldRetry) {
if (kDebugMode) {
print('🔁 Retrying request (${currentRetry + 1}) => ${options.uri}');
}
options.extra['retry'] = currentRetry + 1;
try {
final response = await _dio.fetch(options);
return handler.resolve(response);
} on DioException catch (e) {
return handler.reject(e);
}
}
return handler.next(options);
},
onResponse: (response, handler) {
if (kDebugMode) {
print('📥 RESPONSE[${response.statusCode}] => DATA: ${response.data}');
}
return handler.next(response);
},
onError: (error, handler) {
if (kDebugMode) {
print('❌ ERROR[${error.response?.statusCode}] => MESSAGE: ${error.message}');
}
return handler.next(error);
return handler.reject(err);
},
),
);
// === DIO LOGGING INTERCEPTOR (debug only) ===
// ================= MAIN INTERCEPTOR =================
// Use Dio's built-in QueuedInterceptor for better concurrency control
_dio.interceptors.add(
QueuedInterceptorsWrapper(
onRequest: (options, handler) async {
final token = await LocalPreference.getAccessToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
final requestOptions = error.requestOptions;
try {
// QueuedInterceptor handles concurrency automatically
final refreshed = await _refreshToken();
if (refreshed) {
final newToken = await LocalPreference.getAccessToken();
requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else {
await _forceLogout();
return handler.reject(error);
}
} catch (e) {
await _forceLogout();
return handler.reject(error);
}
}
handler.next(error);
},
),
);
// ================= LOGGING INTERCEPTOR =================
if (kDebugMode) {
_dio.interceptors.add(
LogInterceptor(
request: true,
requestHeader: true,
requestBody: true,
responseHeader: false,
responseBody: true,
error: true,
),
);
}
}
// GET API Request
// ================= GET =================
Future<Response> getApi({
required String url,
Map<String, dynamic>? queryParameters,
@@ -101,18 +122,18 @@ class NetworkApiService {
CancelToken? cancelToken,
}) async {
try {
final response = await _dio.get(
return await _dio.get(
url,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// POST API Request
// ================= POST =================
Future<Response> postApi({
required String url,
dynamic data,
@@ -122,7 +143,7 @@ class NetworkApiService {
ProgressCallback? onSendProgress,
}) async {
try {
final response = await _dio.post(
return await _dio.post(
url,
data: data,
queryParameters: queryParameters,
@@ -130,45 +151,97 @@ class NetworkApiService {
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
// Error Handler
// ================= PUT (NEW) =================
Future<Response> putApi({
required String url,
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
return await _dio.put(
url,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
onSendProgress: onSendProgress,
);
} on DioException catch (e) {
throw _handleError(e);
}
}
// ================= REFRESH TOKEN =================
Future<bool> _refreshToken() async {
try {
final refreshToken = await LocalPreference.getRefreshToken();
if (refreshToken == null) return false;
final response = await _dio.post(
ApiUrls.refreshToken,
data: {
"refreshToken": refreshToken,
},
options: Options(
headers: {
'Authorization': null,
},
),
);
await LocalPreference.setTokens(
accessToken: response.data['accessToken'],
refreshToken: response.data['refreshToken'],
refreshTokenMaxAge: response.data['refreshTokenMaxAge'],
);
return true;
} catch (_) {
return false;
}
}
// ================= LOGOUT =================
Future<void> _forceLogout() async {
await LocalPreference.clearTokens();
await LocalPreference.setLogin(false);
await LocalPreference.resetOnboarding();
// TODO: navigate to login screen
}
// ================= ERROR HANDLER =================
String _handleError(DioException error) {
String errorDescription = "";
switch (error.type) {
case DioExceptionType.connectionTimeout:
errorDescription = "Connection timeout. Please try again.";
break;
return "Connection timeout. Please try again.";
case DioExceptionType.sendTimeout:
errorDescription = "Send timeout. Please try again.";
break;
return "Send timeout. Please try again.";
case DioExceptionType.receiveTimeout:
errorDescription = "Receive timeout. Please try again.";
break;
return "Receive timeout. Please try again.";
case DioExceptionType.badCertificate:
errorDescription = "Bad certificate.";
break;
return "Bad certificate.";
case DioExceptionType.badResponse:
errorDescription = error.response?.data['message'] ??
"Received invalid status code: ${error.response?.statusCode}";
break;
return error.response?.data['message'] ??
"Invalid status code: ${error.response?.statusCode}";
case DioExceptionType.cancel:
errorDescription = "Request was cancelled.";
break;
return "Request was cancelled.";
case DioExceptionType.connectionError:
errorDescription = "No internet connection.";
break;
return "No internet connection.";
case DioExceptionType.unknown:
errorDescription = "Something went wrong. Please try again.";
break;
return "Something went wrong. Please try again.";
}
return errorDescription;
}
// Update headers (e.g., add auth token)
// ================= UPDATE HEADERS =================
void updateHeaders(Map<String, dynamic> headers) {
_dio.options.headers.addAll(headers);
}
}
}