Future function returns value before executing body - flutter

I need help. I have looked everywhere but have no solution.
I have a function to get data from the server (REST API). Let's call this function retrieveProfile(). So when the user navigates to their profile page. I call retrieveProfile() in initState() which works very fine.
Now, I implemented access token and refresh token. So if the access token is expired and the user navigates to their profile page, initState() is fired, retrieveProfile() is fired too. The user is informed that their access token is expired but also that the app is generating a new one. Of course, after generating it, the API is called again through the interceptor. But between this process, the value is returned which is zero. retrieveProfile() itself calls a function that returns a future. I expect all the body to be executed before returning the value. See below:
The function that calls the API and retrieves data from backend
Future<List<AllUsers>> getAllUsersData({
BuildContext? context,
}) async {
_isLoading = true;
notifyListeners();
List<AllUsers> usersList = [];
try {
DatabaseProvider databaseProvider = DatabaseProvider();
final String? token =
await databaseProvider.readAccessToken("accessToken");
final String? refreshToken =
await databaseProvider.readRefreshToken("refreshToken");
if (token == null || token.isEmpty) {
await databaseProvider.saveAccessToken('');
}
http.Response response = await http.get(
Uri.parse('$uri/user/all'),
headers: {
'Authorization': 'Bearer $token',
},
);
if (jsonDecode(response.body)["message"] ==
'Access expired. Try refreshing token.') {
showSnackBar(context!, jsonDecode(response.body)["message"]);
print('refreshing token')
recursively(
message: jsonDecode(response.body)['message'],
token: refreshToken!,
maxRetry: maxRetry,
incrementCounter: setMaxRetry,
action: () async {
await getAllUsersData(context: context);
});
} else {
_maxRetry = 1;
responseHandler(
response: response,
context: context!,
onSuccess: () async {
showSnackBar(
context,
jsonDecode(response.body)['message'],
success: true,
);
_isLoading = false;
_resMessage = jsonDecode(response.body)['message'];
notifyListeners();
// Save all users data
for (int i = 0; i < jsonDecode(response.body)['data'].length; i++) {
usersList.add(
allUsersFromJson(
jsonEncode(
jsonDecode(response.body)['data'][i],
),
),
);
}
},
);
print(usersList.length);
_isLoading = false;
notifyListeners();
}
} on SocketException catch (_) {
_isLoading = false;
_resMessage = 'Connection broken! Number of users may not be up-to-date.';
showSnackBar(context!, _resMessage);
notifyListeners();
} catch (e) {
_isLoading = false;
showSnackBar(context!, e.toString());
_resMessage = e.toString();
notifyListeners();
}
return usersList;
}
The function that is executed in initState to call the async function
List<AllUsers>? allUsers;
void retrieveAllUsers() async {
allUsers = await adminService.getAllUsersData(context: context);
print(allUsers!.length);
setState(() {});
}
#override
void initState() {
super.initState();
retrieveAllUsers();
}
When I print allUsers.length, it returns 0.
When I print usersList.length, it returns 6.
What I expect:
I expect the whole body and if statement executed before returning a value.
On printing, I expect the printing sequence to be:
refreshing token. // This is printed while refreshing token.
6 // from usersList.length after refreshing token and recalling api. Printed when recalling api.
6 // from allUsers.length because the correct value has been returned from the async function. Printed from retrieveProfiles() > initState().
What I get instead:
refreshing token. // This is printed while refreshing token.
0 // from allUsers.length because the correct value has been returned from the async function. Printed from retrieveProfiles() > initState().
6 // from usersList.length after refreshing token and recalling api. Printed when recalling api.
This means that it returns the value immediately after refreshing token without waiting for
getAllUsersData({}) to be recalled. getAllUsersData({}) is recalled in the action callback which is an argument in recursively().
Note that the refreshToken function itself returns a future. And I await it too. The recursively() function calls the refreshToken function.

Related

How to request refresh token called once on multiple api calls

I have a function that refreshes a token if the previous API response returns an error code of 1000. However, when multiple API calls are made at the same time, it results in multiple refresh token requests. I want to ensure that the refresh token is only called once.
Here is my code
requestGet(String endPoint, Map<String, dynamic> params, [bool isUsingToken = false]) async {
String sign = getSign(timestamp + jsonEncode(params));
String deviceId = await SharedPrefsService().getDeviceId();
String token = await SharedPrefsService().getToken();;
final response = await httpGet(endPoint, params, sign, token, deviceId, isUsingToken);
dynamic result = response;
var isRenewed = await renewTokenIfNeeded(deviceId, result, endPoint);
if (isRenewed) {
token = await SharedPrefsService().getToken();
final renewedResponse = await httpGet(endPoint, params, sign, token, deviceId, isUsingToken);
result = renewedResponse;
}
return result;
}
Future<bool> renewTokenIfNeeded(String deviceId, result) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool renewingToken = prefs.getBool('renewingToken') ?? false;
if (result['error_code'] == '1000') {
prefs.setBool('renewingToken', true);
try {
if (renewingToken) {
return true;
}
var isRenewed = await requestRenewToken(deviceId);
if (isRenewed) {
prefs.setBool('renewingToken', false);
return true;
}
} finally {
prefs.setBool('renewingToken', false);
}
}
return false;
}
requestRenewToken(String deviceId) async {
var refresh = await AuthenticationService().refreshToken();
if (refresh.errorCode == '9999') {
SharedPrefsService().clearAllData();
return false; // then back to sign in
}
if (refresh.errorCode == '0000') {
SharedPrefsService().saveTokenData(refresh.token!, refresh.userName!, deviceId);
return true;
}
return false;
}
I have tried using synchronized and mutex packages, but they do not seem to work and I prefer to minimize the use of external packages. Can you please suggest a solution? Thank you!

how to redirect the user to the login page if the token has expired

hello I have a case where when the user token expires the user does not switch to the loginPage page, even though I have set it here.
how do i solve this problem thanks.
i set it on splashscreen if token is not null then go to main page and if token is null then go to login page.
but when the token expires it still remains on the main page
Future<void> toLogin() async {
Timer(
const Duration(seconds: 3),
() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? token = prefs.getString(Constant.token);
Navigator.pushReplacementNamed(
context,
token != null ? AppRoute.mainRoute : AppRoute.loginRoute,
arguments: token,
);
},
);
}
and function when user login
CustomButtonFilled(
title: 'Login',
onPressed: () async {
final prefs =
await SharedPreferences.getInstance();
prefs.setString(Constant.token, '');
if (nimController.text.isEmpty ||
passwordController.text.isEmpty) {
showError('NIM/Password harus diisi');
} else {
setState(() {
isLoading = true;
});
User? user = await userProvider.login(
nimController.text,
passwordController.text);
setState(() {
isLoading = false;
});
if (user == null) {
showError('NIM/Password tidak sesuai!');
} else {
userProvider.user = user;
Navigator.pushNamedAndRemoveUntil(
context,
'/main',
(route) => false,
);
}
}
},
),
and this call api
Future<User?> login(String nim, String password) async {
String url = Constant.baseURL;
try {
var body = {
'username': nim,
'password': password,
};
var response = await http.post(
Uri.parse(
'$url/login_mhs',
),
body: body,
);
if (response.statusCode == 200) {
final token = jsonDecode(response.body)['data']['access_token'];
//Ini mulai nyimpen token
await UtilSharedPreferences.setToken(token);
print(token);
// print(await UtilSharedPreferences.getToken());
return User.fromJson(jsonDecode(response.body));
} else {
return null;
}
} catch (e) {
print(e);
throw Exception();
}
}
you can just make your own HTTP client using Dio and add Interceptor to automatically regenerate idToken if expired using the refreshToken given.
Http client gives an error if the refreshToken also gets expired.
In that case, just navigate to the login screen.
Full code for adding interceptor and making own HTTP client is given below
import 'package:dio/dio.dart';
import '../utils/shared_preference.dart';
class Api {
static Dio? _client;
static Dio clientInstance() {
if (_client == null) {
_client = Dio();
_client!.interceptors
.add(InterceptorsWrapper(onRequest: (options, handler) async {
if (!options.path.contains('http')) {
options.path = 'your-server' + options.path;
}
options.headers['Authorization'] =
'Bearer ${PreferenceUtils.getString('IdToken')}';
return handler.next(options);
}, onError: (DioError error, handler) async {
if ((error.response?.statusCode == 401 &&
error.response?.data['message'] == "Invalid JWT")) {
if (PreferenceUtils.exists('refreshToken')) {
await _refreshToken();
return handler.resolve(await _retry(error.requestOptions));
}
}
return handler.next(error);
}));
}
return _client!;
}
static Future<void> _refreshToken() async {
final refreshToken = PreferenceUtils.getString('refreshToken');
final response = await _client!
.post('/auth/refresh', data: {'refreshToken': refreshToken});
if (response.statusCode == 201) {
// successfully got the new access token
PreferenceUtils.setString('accessToken', response.data);
} else {
// refresh token is wrong so log out user.
PreferenceUtils.deleteAll();
}
}
static Future<Response<dynamic>> _retry(RequestOptions requestOptions) async {
final options = Options(
method: requestOptions.method,
headers: requestOptions.headers,
);
return _client!.request<dynamic>(requestOptions.path,
data: requestOptions.data,
queryParameters: requestOptions.queryParameters,
options: options);
}
}
Dio client = Api.clientInstance();
var resposne = (hit any request);
if(error in response is 401){
//it is sure that 401 is because of expired refresh token as we
//already handled idTokoen expiry case in 401 error while
//adding interceptor.
navigate to login screen for logging in again.
}
Please accept the solution if it solves your problem.
If your session expire feature has some predefine interval or logic than you have to implement it in splash screen and based on that you can navigate user further. Otherwise you want to handle it in API response only you have add condition for statusCode 401.
checkSessionExpire(BuildContext context)
if (response.statusCode == 200) {
//SuccessWork
} else if (response.statusCode == 401) {
//SessionExpire
} else {
return null
}
}

Dio Client: if request to protected route fails (401 code), then refresh the token and try again. Struggling to create

I am trying to create a custom ApiClient class that I can inject as a dependency (with get_it package) to be used in the data layer of my application. In order not to worry about access tokens throughout the presentation/application/domain layers of my app, I'd like to have a field, accessToken, that keeps track of the accessToken inside the ApiClient (singleton) class.
The ApiClient class would be used all throughout my data layer to handle requests to my server for data. It should have a method that allows me to add my own requests to it for unique routes. Then, if those routes require access tokens, it will add the accessToken field from the class along with the request. If that access token is invalid (expired/tampered with), then I would use the refresh token from the device's storage and send a request to the server to get a new access token, then try the original request again. It would "retry" the request at maximum once. Then, if there's still an error, it just returns that to be handled.
I am really struggling with how to implement this. My current attempt is below. Any help would be amazing!
class ApiClient {
final String baseUrl;
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
ApiClient(
{required this.baseUrl,
required this.dio,
required this.networkInfo,
required this.secureStorage}) {
dio.interceptors.add(RefreshInvalidTokenInterceptor(networkInfo, dio, secureStorage));
}
}
class RefreshInvalidTokenInterceptor extends QueuedInterceptor {
final NetworkInfo networkInfo;
final Dio dio;
final FlutterSecureStorage secureStorage;
String? accessToken;
RefreshInvalidTokenInterceptor(this.networkInfo, this.dio, this.secureStorage);
#override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err) && await networkInfo.isConnected) {
try {
// access token request (using refresh token from flutter_secure_storage)
final refreshToken = await secureStorage.read(key: "refreshToken");
final response = await dio.post(
"$kDomain/api/user/token",
queryParameters: {"token": refreshToken},
);
accessToken = response.data["accessToken"];
return err;
} on DioError catch (e) {
handler.next(e);
} catch (e) {
handler.next(err);
}
} else {
handler.next(err);
}
}
bool _shouldRetry(DioError err) =>
(err.response!.statusCode == 403 || err.response!.statusCode == 401);
}
There are similar questions online, but none seem to answer my question! :)
EDIT: I've gotten a working solution (almost), with just 1 error. This works (except in the function retryRequest() I'm hardcoding the request to be a post request):
<imports removed for simplicity>
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
);
dio.interceptors.add(QueuedInterceptorsWrapper(
//! ON REQUEST
onRequest: (options, handler) {
handler.next(options);
},
//! ON RESPONSE
onResponse: (response, handler) {
print("onResponse...");
handler.next(response);
},
//! ON ERROR
onError: (error, handler) async {
print("onError...");
if (tokenInvalid(error)) {
print("token invalid: retrying");
print("header before: ${dio.options.headers}");
await getAccessTokenAndSetToHeader(dio);
print("header after: ${dio.options.headers}");
final response = await retryRequest(error, handler);
handler.resolve(response);
print("here-1");
} else {
handler.reject(error);
}
print("here-2");
print("here-3");
},
));
}
Future<String?> getRefreshToken() async => await secureStorage.read(key: "refreshToken");
Future<void> getAccessTokenAndSetToHeader(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
print("NO REFRESH TOKEN ERROR; LOGOUT!!!");
throw ServerException();
} else {
final response = await dio.post(
"$kDomain/api/user/token",
data: {"token": refreshToken},
);
dio.options.headers["authorization"] = "Bearer ${response.data["accessToken"]}";
}
}
// This function has the hardcoded post
Future<Response> retryRequest(DioError error, ErrorInterceptorHandler handler) async {
print("retry called, headers: ${dio.options.headers}");
final retryResponse = await dio.post(error.requestOptions.path);
print("retry results: $retryResponse");
return retryResponse;
}
bool tokenInvalid(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
Future<void> refreshToken() async {}
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
However, if I change the hardcoded post request to:
final retryResponse =
await dio.request(error.requestOptions.path, data: error.requestOptions.data);
the code no longer works... anyone know why? Having it dynamic based on whatever the failed request was, lets me re-use this class.
package:dio already include the BaseOptions which you can use to add some basic configuration like the baseUrl.
After that, you could use interceptors to add the accessToken to every request. To do this depending on your state management solution you can update the accessToken when the user authentication state changes.
And finally regarding the token refresh you can checkout package:fresh_dio.
Figured it out! (code + how to use below)
Here is my entire ApiClient class (imports hidden for simplicity). It acts as an HTTP client using dio:
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
/// The base options for all requests with this Dio client.
final BaseOptions baseOptions = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
baseUrl: kDomain, // Domain constant (base path).
);
/// Is the current access token valid? Checks if it's null, empty, or expired.
bool get validToken {
if (accessToken == null || accessToken!.isEmpty || Jwt.isExpired(accessToken!)) return false;
return true;
}
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = baseOptions;
dio.interceptors.add(QueuedInterceptorsWrapper(
// Runs before a request happens. If there's no valid access token, it'll
// get a new one before running the request.
onRequest: (options, handler) async {
if (!validToken) {
await getAndSetAccessTokenVariable(dio);
}
setHeader(options);
handler.next(options);
},
// Runs on an error. If this error is a token error (401 or 403), then the access token
// is refreshed and the request is re-run.
onError: (error, handler) async {
if (tokenInvalidResponse(error)) {
await refreshAndRedoRequest(error, handler);
} else {
// Other error occurs (non-token issue).
handler.reject(error);
}
},
));
}
/// Sets the current [accessToken] to request header.
void setHeader(RequestOptions options) =>
options.headers["authorization"] = "Bearer $accessToken";
/// Refreshes access token, sets it to header, and resolves cloned request of the original.
Future<void> refreshAndRedoRequest(DioError error, ErrorInterceptorHandler handler) async {
await getAndSetAccessTokenVariable(dio);
setHeader(error.requestOptions);
handler.resolve(await dio.post(error.requestOptions.path,
data: error.requestOptions.data, options: Options(method: error.requestOptions.method)));
}
/// Gets new access token using the device's refresh token and sets it to [accessToken] class field.
///
/// If the refresh token from the device's storage is null or empty, an [EmptyTokenException] is thrown.
/// This should be handled with care. This means the user has somehow been logged out!
Future<void> getAndSetAccessTokenVariable(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
// User is no longer logged in!
throw EmptyTokenException();
} else {
// New DIO instance so it doesn't get blocked by QueuedInterceptorsWrapper.
// Refreshes token from endpoint.
try {
final response = await Dio(baseOptions).post(
"/api/user/token",
data: {"token": refreshToken},
);
// If refresh fails, throw a custom exception.
if (!validStatusCode(response)) {
throw ServerException();
}
accessToken = response.data["accessToken"];
} on DioError catch (e) {
// Based on the different dio errors, throw custom exception classes.
switch (e.type) {
case DioErrorType.sendTimeout:
throw ConnectionException();
case DioErrorType.connectTimeout:
throw ConnectionException();
case DioErrorType.receiveTimeout:
throw ConnectionException();
case DioErrorType.response:
throw ServerException();
default:
throw ServerException();
}
}
}
}
bool tokenInvalidResponse(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
It should be injected as a singleton to your project so there's one instance of it (for the sake of keeping the state of its accessToken field). I used get_it like so:
// Registers the custom ApiClient class.
sl.registerLazySingleton(() => ApiClient(dio: sl(), networkInfo: sl(), secureStorage: sl()));
Then, inside your data layer (or wherever you call APIs from), you can use it by passing it through the constructor:
class MyDatasource implements IMyDatasource {
final ApiClient apiClient;
late Dio api;
FeedDatasource({required this.client, required this.apiClient}) {
api = apiClient.dio;
}
// Logic for your class here.
}
I simplified it to api so I wouldn't have to go apiClient.dio... every call (optional).
Then, you can use it in one of your class' methods like so:
#override
Future<List<SomeData>> fetchSomeDataFromApi() async {
try {
final response = await api.post("/api/data/whatYouWant");
throw ServerException();
} catch (e) {
throw ServerException();
}
}
Now, for this request, if your class has a valid access token (non-null, non-empty, non-expired), it will call normally. However, if your token isn't valid, it'll refresh it first, then proceed with your call. Even if the call fails after the token originally passed the validation check (token somehow expires during the call for example), it will still be refreshed, and the call re-executed.
Note: I use a lot of custom exceptions, this is optional.
Hopefully this helps someone else!

dart future method with bool return type always returns false

I am calling class method which is in a different file from the main method of main.dart. Here I am trying to get the session status of the user. I am not sure what I am doing wrong, the return value always returns false when called in from the main method, but returns true if printed out in the actual method.
Here the expected result is true as the user is currently in the system and is signed in.
Here is my main method -
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
await Authenticate().getSessionStatus().then((status) => {
print(status)
});
}
Here is my class method -
class Authenticate {
Future<bool> getSessionStatus() async {
bool _isSessionActive = false;
await FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
//print('User is currently signed out!');
_isSessionActive = false;
} else {
//print('User is signed in!');
_isSessionActive = true;
}
});
return _isSessionActive;
}
}
The print statements inside the Authenticate class method, if turned on returns true which is the expected value, but calling the getSessionStatus method from the main method and then printing the value of the status variable always returns false. I believe it has something to do with order in which it is processed, but I am not able to fix it at all.
You can't check if the user is logged with FirebaseAuth.instance.authStateChanges(), since this method only notifies when the user status changes.
You can use the FirebaseAuth.instance.currentUser property instead:
class Authenticate {
Future<bool> getSessionStatus() async {
return FirebaseAuth.instance?.currentUser != null;
}
}
You cannot await the StreamSubscription (which the compiler should warn you about) returned by FirebaseAuth.instance.authStateChanges().listen(...). Below is a minimal example to illustrate what is happening.
void main() async {
bool test = await awaitMe();
print('Main received:' + test.toString());
}
Future<bool> awaitMe() async {
bool innerVal = false;
print('start');
await streamMe().listen((val) {
print('Listener received: ' + val.toString());
innerVal = true;
});
print('end');
return innerVal;
}
Stream<bool> streamMe () async* {
yield true;
}
This will print:
start
end
Main received:false
Listener received: true
So you are actually not awaiting anything.

This expression has a type of 'void' so its value can't be used

I have created a new method to use it at as recursion i am creating function to get refresh token backe from API server
void postLogin(LoginRequest loginRequest) async {
_postStream.sink.add(Result<String>.loading("Loading"));
try {
final response = await client.post('$BASE_URL\/$LOGIN_URL', headers: customHeader, body: json.encode(loginRequest));
print(response.body.toString());
if (response.statusCode == 200) {
_postStream.sink.add(Result<LoginResponse>.success(
LoginResponse.fromJson(json.decode(response.body))));
} else if(response.statusCode == 401) {
getAuthUpadate(postLogin(loginRequest));//error is showing at this place
}
else{
_postStream.sink.add(Result.error(REQUEST_ERROR));
}
} catch (error) {
print(error.toString());
_postStream.sink.add(Result.error(INTERNET_ERROR));
}
}
//////////////
i tried this function inside function in flutter it saying
void cant be allowed
getAuthUpadate(Function function()) async {
try{
final response = await client.post('$BASE_URL\/NEW_ACCESS_TOKEN_PATH', headers: customHeader,body:json.encode('RefreshToken'));
//get the refreshToken from response
String accessToken = response.body.toString();
//store this access token to the data
function();
}
catch (error){
}
}
I could give you an idea about this issue.
You are just calling the function which has return value void and passing it as a parameter to the function getAuthUpdate.
You can try something like this,
getAuthUpadate(Function function, LoginRequest loginRequest) async { }
and in the postLogin method, you could try something like this,
else if(response.statusCode == 401) {
getAuthUpadate(postLogin, loginRequest); // Just pass Reference as parameter!
}
finally, the iteration part of getAuthUpdate method,
try{
final response = await client.post('$BASE_URL\/NEW_ACCESS_TOKEN_PATH', headers: customHeader,body:json.encode('RefreshToken'));
//get the refreshToken from response
String accessToken = response.body.toString();
//store this access token to the data
function(loginRequest);
}
catch (error){
}
Hope that works!
This error happened to me when I passed in a void in a callback method (e.g. calling function() when function is declared to be void function() { ... }.
For example:
IconButton(
icon: Icon(
MdiIcons.plusCircle,
),
onPressed: _navigateToJoinQuizScreen()); // <--- Wrong, should just be _navigateToJoinQuizScreen