How to cache authorization Data(access_token)? - flutter

I have macOS app and I want to cache data when user sign in or sign up. I have access_token and refresh_token.
And when user launch Mac OS app again, I want to make user won't sign in again and immediately transfer to MainWidget()
For storing access_token and refresh_token, I use shared_preferences
This is my PreferencesProvider:
class PreferencesProvider {
static PreferencesProvider? _instance;
SharedPreferences? _preferences;
factory PreferencesProvider() {
_instance ??= PreferencesProvider._();
return _instance!;
}
PreferencesProvider._() {
_initPreferences();
}
Future<void> _initPreferences() async {
_preferences = await SharedPreferences.getInstance();
}
Future<void> setAccess(String value) async {
await _initPreferences();
await _preferences!.setString("ACCESS_TOKEN", value);
}
Future<String?> getAccess() async {
await _initPreferences();
return _preferences!.getString("ACCESS_TOKEN");
}
Future<void> clearAccess() async {
await _initPreferences();
await _preferences!.remove("ACCESS_TOKEN");
}
Future<void> setRefresh(String value) async {
await _initPreferences();
await _preferences!.setString("REFRESH_TOKEN", value);
}
Future<String?> getRefresh() async {
await _initPreferences();
return _preferences!.getString("REFRESH_TOKEN");
}
}
And this is my authorization cubit when I sign in or Sign up(if authorization is successful):
if (data['success']) {
await _preference.clearAccess();
PreferencesProvider()
..setUserId(data['userId'])
..setAccess(data['accessToken'])
..setRefresh(data['refreshToken']);
_apiProvider.setToken(data["accessToken"]);
}
And this is what I show in Main App: home: const StartScreen(),
When I launched my Mac OS app and if I signed in then I want to show home: MainWidget()

As the home: of your MaterialApp, always assign an intermediate screen[whether or not you are logged in], in this screen we will check if the user is logged in or not and redirect him to respective screens. Let's say the intermediate screen is SplashScreen which could look something like this:
class SplashScreen extends StatefulWidget {
const SplashScreen({Key? key}) : super(key: key);
#override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
Future<bool> _checkLoginStatus() async {
try {
//check whether we have access and refresh tokens here (maybe also perform
// a network request to check if the tokens are still valid) and
// based on that, return true or false
} catch (e) {
return false;
}
}
#override
void initState() {
_checkLoginStatus().then((value) {
if (value == true) {
//navigate the user to MainWidget()
} else {
//navigate the user to StartScreen()
}
});
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
}
PS: if you intend to save your tokens locally, consider using packages like flutter_secure_storage or hive so that you can encrypt your data while saving.

Related

how to await for network connectivity status in flutter

I have used connectivity_plus and internet_connection_checker packages to check the internet connectivity.
The problem occured is , the app works perfectly fine as expected when the app start's with internet on state. But when the app is opened with internet off, the dialog isn't shown !!
I assume this is happening because the build method is called before the stream of internet is listened.
Code :
class _HomePageState extends State<HomePage> {
late StreamSubscription subscription;
bool isDeviceConnected = false;
bool isAlertSet = false;
#override
void initState() {
getConnectivity();
super.initState();
}
getConnectivity() {
subscription = Connectivity().onConnectivityChanged.listen(
(ConnectivityResult result) async {
isDeviceConnected = await InternetConnectionChecker().hasConnection;
if (!isDeviceConnected && isAlertSet == false) {
showDialogBox();
setState(() {
isAlertSet = true;
});
}
},
);
}
#override
void dispose() {
subscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
...
);
}
showDialogBox() => showDialog(/* no internet dialog */)
Extending the question: Is it assured that this works for all the pages ?
if yes, how ?
if not , how to overcome this?
First of all you need to listen for internet connectivity in your app first screen which is probably app.dart
GlobalKey<NavigatorState> navigatorKey = GlobalKey();
final noInternet = NoInternetDialog();
class TestApp extends StatefulWidget {
#override
State<TestApp> createState() => _TestAppState();
}
class _TestAppState extends State<TestApp> {
#override
void initState() {
super.initState();
checkInternetConnectivity();
}
#override
Widget build(BuildContext context) {
return MaterialApp(...);
}
Future<void> checkInternetConnectivity() async {
Connectivity().onConnectivityChanged.getInternetStatus().listen((event)
{
if (event == InternetConnectionStatus.disconnected) {
if (!noInternet.isShowing) {
noInternet.showNoInternet();
}
}
});
}
}
Make the screen stateful in which you are calling MaterialApp and in initState of that class check for your internet connection, like above
You are saying how can I show dialog when internet connection changes for that you have to create a Generic class or extension which you can on connectivity change. You have to pass context to that dialogue using NavigatorKey
class NoInternetDialog {
bool _isShowing = false;
NoInternetDialog();
void dismiss() {
navigatorKey.currentState?.pop();
}
bool get isShowing => _isShowing;
set setIsShowing(bool value) {
_isShowing = value;
}
Future showNoInternet() {
return showDialog(
context: navigatorKey.currentState!.overlay!.context,
barrierDismissible: true,
barrierColor: Colors.white.withOpacity(0),
builder: (ctx) {
setIsShowing = true;
return AlertDialog(
elevation: 0,
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.all(3.0.h),
content: Container(...),
);
},
);
}
}
Use checkConnectivity to check current status. Only changes are exposed to the stream.
final connectivityResult = await Connectivity().checkConnectivity();

How can I use condition and go to different page on flutter using Getx without click

I am trying to use firebase data to route different pages using Getx. First I have a splash screen and want to automatically go to different pages according to conditions. If the user has already login, it will redirect the Home page, if not the route to the login page. But I can't use initState() on the Stateless widget as I using Getx, I don't want a Stateful widget.
class SplashPage extends StatelessWidget {
RxBool isloading = true.obs;
#override
Widget build(BuildContext context) {
String Uid = "";
return isloading.value
? SpinKitThreeInOut(
color: Colors.red,
)
: Obx(() {
return Get.find<AuthController>().user != null
? homeMethod()
: login();
});
}
Widget homeMethod() {
return Home(AuthController.instance.user.toString());
isloading.value = false;
}
}
But I ain't able to override isloading.value = false;
My Getx Auth Controller:
class AuthController extends GetxController {
static AuthController instance = Get.find();
FirebaseAuth auth = FirebaseAuth.instance;
Rxn<User> _firebaseUser = Rxn<User>();
String? get user => _firebaseUser.value?.uid;
#override
void onReady() {
// TODO: implement onReady
super.onReady();
_firebaseUser.value = auth.currentUser;
_firebaseUser.bindStream(auth.userChanges());
ever(_firebaseUser, _initialScreen);
}
/* #override
void onInit() {
_firebaseUser.bindStream(_auth.authStateChanges());
}*/
_initialScreen(User? user) {
if (user == null) {
Get.offAll(login());
} else {
String userId = user.uid;
Get.offAll(Home(userId));
}
}
Future<User?> LogInAccounts(String Email, String Password) async {
FirebaseAuth auth = FirebaseAuth.instance;
try {
User? user = (await auth.signInWithEmailAndPassword(
email: Email, password: Password))
.user;
if (user != null) {
Fluttertoast.showToast(msg: "Account Create Sucessfully");
return user;
} else {
Fluttertoast.showToast(msg: "Account Create Failed!");
return user;
}
} catch (e) {
return null;
}
}
}
Updated Answer
You can use bindStream and do it that way, but instead of trying to turn your User object into a stream this can be done with a simple RxBool. Firebase already provides a function to listen to auth state changes.
class AuthController extends GetxController {
RxBool loggedIn = false.obs;
#override
void onInit() {
super.onInit();
_subscribe();
}
void _subscribe() {
FirebaseAuth.instance.authStateChanges().listen((User? user) {
if (user == null) {
loggedIn(false);
log('User is currently signed out');
} else {
loggedIn(true);
log('User is signed in');
}
});
}
}
Then you can add another couple methods to your GetX class.
void initNaviationListener() {
/// inital startup naviation
_navigateBasedOnLogin();
/// future navigation based on auth state changes
ever(loggedIn, (value) {
_navigateBasedOnLogin();
});
}
void _navigateBasedOnLogin() {
if (loggedIn.value == false) {
Get.offAndToNamed(LoginPage.id);
} else {
Get.offAndToNamed(HomePage.id);
}
}
Then you can call initNaviationListener in the onReady of GetMaterialApp
GetMaterialApp(
/// onReady is called after GetMaterialApp is fully initialized
onReady: () => Get.find<AuthController>().initNaviationListener(),
theme: ThemeData.dark(),
initialRoute: LoginPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
That will navigate on app start to the corresponding screen and also respond to any future changes in auth status.
Original Answer
You don't have to navigate from the SplashPage you can do it from the controller.
Let's say your GetMaterialApp looks like this. This takes you to SplashPage first.
GetMaterialApp(
initialRoute: SplashPage.id,
getPages: [
GetPage(
name: SplashPage.id,
page: () => SplashPage(),
),
GetPage(
name: HomePage.id,
page: () => HomePage(),
),
GetPage(
name: LoginPage.id,
page: () => LoginPage(),
),
],
)
Then check logged in status and navigate to the corresponding screen from your AuthController.
class AuthController extends GetxController {
#override
void onInit() {
super.onInit();
_navigateBasedOnLogin();
}
Future<void> _navigateBasedOnLogin() async {
final loggedIn = await _isLoggedIn();
if (loggedIn) {
Get.offAndToNamed(HomePage.id); // offAndToNamed will remove the SplashScreen from the navigation stack
} else {
Get.offAndToNamed(LoginPage.id);
}
}
Future<bool> _isLoggedIn() async {
/// run your code to check logged in status and return true or false
}
}
Then just init the AuthController in your main.
void main() async {
Get.put(AuthController());
runApp(MyApp());
}
With this setup, your SplashScreen can be a generic loading screen with zero logic.
You can handle initialRoute of GetMaterialApp using isLogin flag
class _MyAppState extends State<MyApp> {
bool isLogin = false;
#override
void initState() {
isLogin = isAlreadyLogin();// Your function to check is user logged in.
super.initState();
}
#override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Rider App',
translationsKeys: AppTranslation.translationsKeys,
locale: Get.find<CacheManager>().getLocale(),
getPages: AppPages.pages,
initialRoute: isLogin ? Routes.homeScreen : Routes.loginScreen,
initialBinding: InitialBinding(),
);
}
class Routes {
static const homeScreen = '/home-screen';
static const loginScreen = '/login-screen';
}
class AuthController extends GetxController {
late Rx<User?> firebaseUser;
#override
void onReady() async {
super.onReady();
firebaseUser = Rx<User?>(FirebaseAuth.instance.currentUser);
firebaseUser.bindStream(firebaseAuth.instance.userChanges());
ever(firebaseUser, _setInitialScreen);
}
_setInitialScreen(user) async{
if (user != null) {
Get.offAllNamed(Routes.home);
} else {
Get.offAllNamed(Routes.login);
}
}
}

shared preference + firebase auth not working properly

Firebase Auth is working properly, I'm able to log in, Sign in but I want to preserve the state of the application. For state persistence, I'm using Share preferences.
I'm using sharedpreferences. I made a class and defined some keys and methods to fetch and set the data in the keys at the time of login and use it later in the app.
Please help me out with an easy approch how to do this.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
class CurrentUser {
static SharedPreferences? mypreferences;
static const emailKey = 'emailKey';
static const passwordKey = 'paswordKey';
static const loginStatusKey = 'loginStatusKey';
static Future init() async {
mypreferences = await SharedPreferences.getInstance();
}
static Future setUserEmail(String? emailValue) async {
return await mypreferences?.setString(emailKey, emailValue!);
}
static Future setUserPassword(String? passwordValue) async {
return await mypreferences?.setString(passwordKey, passwordValue!);
}
static Future setLoginStatus(bool status) async {
return await mypreferences?.setBool(loginStatusKey, status);
}
static String? getUserEmail() {
return mypreferences?.getString(emailKey);
}
static String? getUserPassword() {
return mypreferences?.getString(passwordKey);
}
static bool? getUserLoginStatus(){
if(loginStatusKey==null){
mypreferences?.setBool(loginStatusKey, false);
}
return mypreferences?.getBool(loginStatusKey);
}
}```
// storing the value during login //
``` myLogIn() async {
try {
UserCredential myuser= await FirebaseAuth.instance
.signInWithEmailAndPassword(email: email, password: password);
// print(myuser);
// print(myuser.user?.email);
CurrentUser.setUserEmail(myuser.user?.email);
CurrentUser.setUserPassword(password);
CurrentUser.setLoginStatus(true);
Navigator.pushReplacementNamed(context, MyRoutes.homeRoute);
} on FirebaseAuthException catch (e) {
if (e.code == 'user-not-found') {
print('user is not registered');
ScaffoldMessenger.of(context).showSnackBar(
MySnackBar.showcustomSnackbar('user is not registered'));
} else if (e.code == 'wrong-password') {
print('wrong password');
ScaffoldMessenger.of(context)
.showSnackBar(MySnackBar.showcustomSnackbar('wrong password'));
}
}
}```
// using the store login status in main file to show login page and home page accordingly.
```import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
// import 'firebase_options.dart';
import './pages/login.dart';
import './pages/signup.dart';
import './pages/homepage.dart';
import './utils/my_theme.dart';
import './utils/my_routes.dart';
import './pages/forgot_password.dart';
import './utils/my_user.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
CurrentUser.init();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// const MyApp({ Key? key }) : super(key: key);
final Future<FirebaseApp> initializeMyFirebaseApp = Firebase.initializeApp();
bool? isLogin = CurrentUser.getUserLoginStatus();
#override
void initState() {
// TODO: implement initState
isLogin = CurrentUser.getUserLoginStatus();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: initializeMyFirebaseApp,
builder: (context, snaphot) {
//Error checking
if (snaphot.hasError) {
print("Something went wrong!");
}
//If snapshot state is in waiting or so
if (snaphot.connectionState == ConnectionState.waiting) {
return Center(
child: CircularProgressIndicator(),
);
}
//else we will show the normal login page.
return MaterialApp(
theme: MyTheme.lightTheme(context),
//isUserLoggedIn !=null ? isUserLoggedIn ? Home() : SignUp() : SignIn(),
home:isLogin!=null ?
isLogin! ? HomePage() : Login() : Login(),
// Login(),
routes: {
// "/":(context) => Login(),
MyRoutes.loginRoute: (context) => Login(),
MyRoutes.signupRoute: (context) => SignUp(),
MyRoutes.homeRoute: (context) => HomePage(),
MyRoutes.forgotpasswordRoute: (context) => ForgotPassword(),
},
);
});
}
}
```

How to change the current mediaItem when the next mediaItem in the queue is played

I'm developing an iOS app in Flutter, using a package called audio_service.
I use AudioServiceBackground.serQueue() to set multiple MediaItem to a Queue.
In the UI part, I'm trying to use AudioService.currentMediaItemStream to display the information of the currently playing media item.
When the first song in the queue is finished, the second song will be played. However, the information on the current media item does not change.
How do I detect that the song playing in the Queue has changed?
class AudioServiceScreen extends StatefulWidget {
#override
_AudioServiceScreenState createState() => _AudioServiceScreenState();
}
class _AudioServiceScreenState extends State<AudioServiceScreen> {
#override
void initState() {
super.initState();
Future(() async {
await AudioService.connect();
await start();
});
}
#override
void dispose() {
Future(() async {
await AudioService.disconnect();
});
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: ///,
body: StreamBuilder<MediaItem?>(
stream: AudioService.currentMediaItemStream,
builder: (context, snapshot) {
final mediaItem = snapshot.data;
},
),
);
}
Future<dynamic> start() async {
final success = await AudioService.start(
backgroundTaskEntrypoint: _backgroundTaskEntrypoint,
);
if (success) {
await updateQueue();
}
}
Future<void> updateQueue() async {
final queue = await getMediaLibrary(); // get data from FireStore
await AudioService.updateQueue(queue);
}
}
void _backgroundTaskEntrypoint() {
AudioServiceBackground.run(() => AudioPlayerTask());
}
class AudioPlayerTask extends BackgroundAudioTask {
final AudioPlayer audioPlayer = AudioPlayer();
#override
Future<void> onStart(Map<String, dynamic>? params) async {
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());
await AudioServiceBackground.setState(
controls: [MediaControl.pause, MediaControl.stop],
playing: false,
processingState: AudioProcessingState.connecting,
);
}
#override
Future<void> onUpdateQueue(List<MediaItem> queue) async {
await AudioServiceBackground.setQueue(queue);
try {
await audioPlayer.setAudioSource(ConcatenatingAudioSource(
children:
queue.map((item) => AudioSource.uri(Uri.parse(item.id))).toList(),
));
} on Exception catch (e) {
await onStop();
}
}
}
Try read the docs and follow some of the example there. Because on "Background Code" section, there is "on Start" code. Maybe that code can help you out. I am sorry I cannot help you that much. I never try that package.

How to Navigate one of the two screens to users correctly in Flutter?

Recently I am developing first ever app in Flutter and I know the basics only.
So here's the problem which I am facing currently.
I have to Navigate One of the two screens, either Register or Home page according to old/new user, So what I did is here.
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'Register.dart';
import 'HomePage.dart';
class Authenticate extends StatefulWidget {
#override
_AuthenticateState createState() => _AuthenticateState();
}
class _AuthenticateState extends State<Authenticate> {
String _userName = "";
#override
void initState() {
super.initState();
_getUserName();
}
_getUserName() async {
SharedPreferences pref = await SharedPreferences.getInstance();
setState(() {
_userName = pref.getString('userName') ?? "";
});
}
Widget build(BuildContext context) {
if (_userName == "") {
print('name : $_userName , NEW user'); // ERROR - why it's printing on console for old User ?
return Register();
} else {
return HomePage(_userName);
}
}
}
So the problem is, even if I am opening app through old user, it is printing the debug code written for new user and there's around 0.3 sec screen visibility of Register screen.
So what should I do to fix this?
The print is happening because this line of code:
SharedPreferences pref = await SharedPreferences.getInstance();
is asynchronous, which means that it might invoke in some later point in time. Because of that the build method is called first, then your _getUserName method finished, which causes a setState and invokes a build method again.
You might want to show some kind of loader screen until sharedPrefernces is initialized and then decide wheter to show Register or Home page.
Example code:
class _AuthenticateState extends State<Authenticate> {
String _userName = "";
bool _isLoading = true;
#override
void initState() {
super.initState();
_getUserName();
}
_getUserName() async {
SharedPreferences pref = await SharedPreferences.getInstance();
setState(() {
_userName = pref.getString('userName') ?? "";
_isLoading = false;
});
}
Widget build(BuildContext context) {
if (_isLoading) {
return Center(child: CupertinoActivityIndicator());
} else if (_userName == "") {
print('name : $_userName , NEW user'); // ERROR - why it's printing on console for old User ?
return Register();
} else {
return HomePage(_userName);
}
}
}
Try this:
class _AuthenticateState extends State<Authenticate> {
String _userName;
#override
void initState() {
super.initState();
_getUserName();
}
_getUserName() async {
SharedPreferences pref = await SharedPreferences.getInstance();
setState(() {
_userName = pref.getString('userName') ?? '';
});
}
Widget build(BuildContext context) {
if (_userName == null) {
return Center(child: CircularProgressIndicator());
} else if (_userName == '') {
print('name : $_userName , NEW user');
return Register();
} else {
return HomePage(_userName);
}
}
}
In this case, _userName is null initially and will only be assigned a value after the _getUserName() returns either an empty String or an old userName. When null, the widget will build a progress indicator instead. If you don't want that, just return a Container().