How to emit Bloc State based on retry request response? - flutter

I tried to implement a refresh token in my app, so every time I do request, dio will check the response, if the response status code is 401 then it will refresh the access token and retry the request
Future onError(DioError dioError, ErrorInterceptorHandler handler) async {
print(dioError.response!.statusCode);
int? responseCode = dioError.response!.statusCode;
String? oldAccessToken = _sharedPreferencesManager
.getString(SharedPreferencesManager.keyAccessToken);
if (oldAccessToken != null && responseCode == 401) {
_dio.interceptors.responseLock.lock();
_dio.interceptors.responseLock.lock();
String? refreshToken = _sharedPreferencesManager
.getString(SharedPreferencesManager.keyRefreshToken);
Map<String, dynamic> refreshTokenBody = {
'grant_type': 'refresh_token',
'refresh_token': refreshToken
};
ApiRepository apiAuthRepository = ApiRepository();
LoginModel token =
await apiAuthRepository.postRefreshAuth(refreshTokenBody);
if (token.success) {
String newAccessToken = token.data.accessToken;
String newRefreshToken = token.data.refreshToken;
await _sharedPreferencesManager.putString(
SharedPreferencesManager.keyAccessToken, newAccessToken);
await _sharedPreferencesManager.putString(
SharedPreferencesManager.keyRefreshToken, newRefreshToken);
}
RequestOptions options = dioError.response!.requestOptions;
options.headers.addAll({'requirestoken': true});
_dio.interceptors.requestLock.unlock();
_dio.interceptors.responseLock.unlock();
handler.next(dioError);
return _dio.request(options.path,
options: Options(headers: options.headers));
} else {
super.onError(dioError, handler);
}
}
The problem is in my Bloc, if I tried to access the API with the expired access token (If I tried using an active access token there is no problem), the dio works fine by refreshing and retrying the request, but my bloc only emits a Loading/Failure State based on the result of the first response, any idea how to solve it?
Future<void> mapEventToState(
Emitter<DashboardState> emit, DashboardEvent event) async {
emit(DashboardLoading());
DashboardModel getMe = await apiAuthRepository.getMeUser();
if (!getMe.success) {
emit(DashboardFailure(getMe.message));
}
emit(
DashboardSuccess(),
);
}
And here is how I implement the Bloc
BlocProvider<DashboardBloc>(
create: (_) => _dashboardBloc,
child: BlocListener<DashboardBloc, DashboardState>(
listener: (context, state) {
if (state is DashboardFailure)
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(state.error),
));
},
child: BlocBuilder<DashboardBloc, DashboardState>(
builder: (context, state) {
if (state is DashboardSuccess)
return ListView(..);
else if (state is DashboardLoading)
return Center(child: const CircularProgressIndicator());
else
print(state);
return Container();

Related

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

Dialog shows up several times (Does FirebaseDynamicLinks.instance.onLink.listen stay in memory after AppLifecycleState.paused/inactive in flutter?)

I have "Email link sign-in (Passwordless)" in my app. It works as expected. But there is one issue that the catchError on _auth.signInWithEmailLink(email: userEmail, emailLink: link.toString()) is called several times (e.g. if I go to my email, use an expired action code for the 4th time, when it navigates to the app, it opens 4 dialogs for that error).
Here is the code in didChangeAppLifecycleState
void didChangeAppLifecycleState(AppLifecycleState state) async {
FirebaseDynamicLinks.instance.onLink.listen((dynamicLink) {
final Uri? deepLink = dynamicLink.link;
emailLinkService.handleLink(
deepLink!, _emailController.text, context, state);
if (state == AppLifecycleState.paused) {
Navigator.popUntil(context, (route) => !(route is PopupRoute));
}
}, onError: (e) async {
print(e);
});
final PendingDynamicLinkData? data =
await FirebaseDynamicLinks.instance.getInitialLink();
final Uri? deepLink = data?.link;
if (deepLink != null) {
emailLinkService.observableLink = deepLink.toString();
}
}
And here is the handleLink method:
class EmailLinkService {
final FirebaseAuth _auth = FirebaseAuth.instance;
Future handleLink(Uri link, userEmail, BuildContext context, state) async {
if (_auth.isSignInWithEmailLink(link.toString())) {
_auth
.signInWithEmailLink(email: userEmail, emailLink: link.toString())
.catchError((onError) {
if (state == AppLifecycleState.resumed) {
PlatformAlertDialog(
title: onError.code,
content: onError.message ?? onError.toString(),
defaultActionText: Strings.ok,
).show(context);
}
print(
'>>>> email link service > handleLink > Error signing in with email link $onError');
});
}
}
}
I expect to get one dialog each time when there is an error.

Flutter http request from Rapid Api not loading

I am tying retrieve data from an api on Rapid Api using Dart's http package and displaying it using Flutter however the content never loads and the api doesn't return an error.
class APIService {
// API key
static const _api_key = <MYAPIKEY>;
// Base API url
static const String _baseUrl = "covid-19-data.p.rapidapi.com";
// Base headers for Response url
static const Map<String, String> _headers = {
"content-type": "application/json",
"x-rapidapi-host": "covid-19-data.p.rapidapi.com",
"x-rapidapi-key": _api_key,
};
Future<CovidNumbers> fetchData(
{#required String endpoint, #required Map<String, String> query}) async {
Uri uri = Uri.https(_baseUrl, endpoint, query);
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
return CovidNumbers.fromJson(json.decode(response.body));
} else {
throw Exception('Failed to load Data');
}
}
}
The method is then called onInit
Future<CovidNumbers> data;
APIService apiService = APIService();
#override
void initState() {
super.initState();
data = apiService.fetchData(
endpoint: "/country", query: {"format": "json", "name": "malta"});
}
And finally I display it in a FutureBuilder
FutureBuilder<CovidNumbers>(
//future: futureCovidNumbers,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
"Confirmed Cases: ${snapshot.data.confirmed.toString()}");
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
return CircularProgressIndicator();
},
));
The app remains stuck on the CircularProgressIndicator and does not display an error.
you future is empty, for that reason always is returning a CircularProgressIndicator, place your "data" variable inside the future and try again

How to do stream builder to get data from bloc in flutter

I am new in BLOC and I am trying to read respond from api.. but whenever I call stream builder... my widget always stops in wait... here is my code
here is api provider file
class Provider {
final _url = '...';
Future<List<LoginRespon>> login(a, b) async {
List<LoginRespon> datalogin = [];
try {
bool trustSelfSigned = true;
HttpClient httpClient = new HttpClient()
..badCertificateCallback =
((X509Certificate cert, String host, int port) =>
trustSelfSigned);
IOClient http = new IOClient(httpClient);
final response = await http.post(_url,
headers: {
HttpHeaders.contentTypeHeader: 'application/json',
},
body: json.encode({
"aa": a,
"bb": b,
}));
Map<String, dynamic> responseJson = json.decode(response.body);
if (responseJson["status"] == "200") {
datalogin.add(LoginRespon(
status: responseJson['status'],
data: Data(
name: responseJson['data']['name'],
position: responseJson['data']['pos'])));
return datalogin;
} else {
print("ppp");
}
} on Exception {
rethrow;
}
return datalogin;
}
}
and here is for stream builder
isClick
? StreamBuilder(
stream: bloc.login(),
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data);
return Text("success");
} else if (snapshot.hasError) {
return Text(
snapshot.error.toString());
}
return Text("wait..");
},
)
: FlatButton(
child: Text("Login"),
onPressed: () {
setState(() {
isClick = true;
});
},
),
is there a way so that I can call print(snapshot.data) inside if (snapshot.hasData)
You need to pass argument which required in method otherwise it will not successfully responce (200) and it will throw error.

Why I getting another connectionState type after connectionState.done in a StreamBuilder?

I have created a StreamController to handle authentication. I subscribe a user when the sign in is completed. So I create a class for that:
class AuthAPI {
final FacebookLogin facebookLogin = FacebookLogin();
final Dio _dio = Dio();
final StreamController<User> _authStatusController = StreamController<User>.broadcast();
Stream<User> get onAuthStatusChanged => _authStatusController.stream;
// Facebook Sign In
Future<User> facebookSignIn() async {
FacebookLoginResult result = await facebookLogin.logIn(['public_profile', 'email']);
switch(result.status) {
case FacebookLoginStatus.loggedIn:
return _sendFacebookUserDataToAPI(result);
case FacebookLoginStatus.error:
return null;
case FacebookLoginStatus.cancelledByUser:
print('Cancelled');
return null;
default:
return null;
}
}
// Sign Out
void signOut() async {
facebookLogin.logOut();
_authStatusController.sink.add(null);
_authStatusController.close();
}
Future<User> _sendFacebookUserDataToAPI(FacebookLoginResult result) async {
final String facebookToken = result.accessToken.token;
final Response graphResponse = await _dio.get(
'https://graph.facebook.com/v4.0/me?fields='
'first_name,last_name,email,picture.height(200)&access_token=$facebookToken');
final profile = jsonDecode(graphResponse.data);
ApiProvider apiProvider = ApiProvider();
UserSocialAuth userSocialAuth = UserSocialAuth(
firstName: profile['first_name'],
lastName: profile['last_name'],
email: profile['email'],
provider: 'facebook',
providerUserId: profile['id']
);
Map socialSignIn = await apiProvider.socialSignIn(userSocialAuth);
User user;
if (socialSignIn.containsKey('access_token')) {
Map userData = await apiProvider.currentUser(socialSignIn['access_token']);
user = User.fromJson(userData['data']);
apiProvider.setAccessToken(socialSignIn['access_token']);
_authStatusController.sink.add(user);
print("Login Successful");
} else {
_authStatusController.sink.addError(socialSignIn['error']);
}
_authStatusController.close();
return user;
}
}
and this is my StreamBuilder:
return StreamBuilder(
stream: userBloc.authStatus,
builder: (BuildContext context, AsyncSnapshot snapshot) {
print(snapshot.connectionState);
switch(snapshot.connectionState) {
case ConnectionState.active:
User user = snapshot.data;
if (user == null) {
return SignInSignUpScreen();
}
return _showHomeUI(user, snapshot);
case ConnectionState.done:
User user = snapshot.data;
if (user == null) {
return SignInSignUpScreen();
}
print(user);
return _showHomeUI(user, snapshot);
default:
return Center(child: CircularProgressIndicator());
}
}
);
So, when I make the login, then it shows a CircularProgressIndicator, and if the authentication is successful, then it has to show the home screen. But, it stills showing the login screen, and when I print the output of the connectionState, I see that after the connectionState.done, the connectionState pass to connectionState.waiting and I do not know why.
Here is the output of the console:
And when it reaches to the last connectionState.done, it does not have data.
You're calling _authStatusController.close(); in the end of _sendFacebookUserDataToAPI method – that means that the underlying stream is finished and you stream listener enters "done" state.
You should instead create e.g. dispose() method in AuthAPI class and call _authStatusController.close() there. This method should be called when AuthAPI is no longer needed.