Flutter: login through a webview - authentication

I'm pretty new to Flutter.
Is there a way to login through a webview into our app?
e.g. In the first page there is this webview where we can do the login. After we logged in, the app takes us in a second page where we can do other stuff.

In my app I use instagram implicit authentification, which implies to login user in webview and get token from redirect url. I use flutter_webview_plugin
Next code builds WebviewScaffold with login url. And it listen for url changes. So when response is redirected to my redirectUrl it parses url to get token. Then you need to save token for following requests in app.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_webview_plugin/flutter_webview_plugin.dart';
class LoginScreen extends StatefulWidget {
#override
_LoginScreenState createState() => new _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final flutterWebviewPlugin = new FlutterWebviewPlugin();
StreamSubscription _onDestroy;
StreamSubscription<String> _onUrlChanged;
StreamSubscription<WebViewStateChanged> _onStateChanged;
String token;
#override
void dispose() {
// Every listener should be canceled, the same should be done with this stream.
_onDestroy.cancel();
_onUrlChanged.cancel();
_onStateChanged.cancel();
flutterWebviewPlugin.dispose();
super.dispose();
}
#override
void initState() {
super.initState();
flutterWebviewPlugin.close();
// Add a listener to on destroy WebView, so you can make came actions.
_onDestroy = flutterWebviewPlugin.onDestroy.listen((_) {
print("destroy");
});
_onStateChanged =
flutterWebviewPlugin.onStateChanged.listen((WebViewStateChanged state) {
print("onStateChanged: ${state.type} ${state.url}");
});
// Add a listener to on url changed
_onUrlChanged = flutterWebviewPlugin.onUrlChanged.listen((String url) {
if (mounted) {
setState(() {
print("URL changed: $url");
if (url.startsWith(Constants.redirectUri)) {
RegExp regExp = new RegExp("#access_token=(.*)");
this.token = regExp.firstMatch(url)?.group(1);
print("token $token");
saveToken(token);
Navigator.of(context).pushNamedAndRemoveUntil(
"/home", (Route<dynamic> route) => false);
flutterWebviewPlugin.close();
}
});
}
});
}
#override
Widget build(BuildContext context) {
String loginUrl = "someservise.com/auth";
return new WebviewScaffold(
url: loginUrl,
appBar: new AppBar(
title: new Text("Login to someservise..."),
));
}
}

You can use my plugin flutter_inappwebview, which is a Flutter plugin that allows you to add inline WebViews or open an in-app browser window and has a lot of events, methods, and options to control WebViews.
You can use onLoadStartĀ or onLoadStop eventsĀ to detect URL changes. For example, you can get the token:
from the url
from cookies
from localStorage
Full example:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future main() async {
runApp(new MyApp());
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: InAppWebViewPage()
);
}
}
class InAppWebViewPage extends StatefulWidget {
#override
_InAppWebViewPageState createState() => new _InAppWebViewPageState();
}
class _InAppWebViewPageState extends State<InAppWebViewPage> {
InAppWebViewController webView;
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("InAppWebView")
),
body: Container(
child: Column(children: <Widget>[
Expanded(
child: Container(
child: InAppWebView(
initialUrlRequest: URLRequest(url: Uri.https("next.kemi.ai", "")),
initialOptions: InAppWebViewGroupOptions(
android: AndroidInAppWebViewOptions(
domStorageEnabled: true,
databaseEnabled: true,
),
),
onWebViewCreated: (InAppWebViewController controller) {
webView = controller;
},
onLoadStart: (InAppWebViewController controller, Uri? url) {
},
onLoadStop: (InAppWebViewController controller, Uri? url) async {
if (url?.toString().startsWith("https://myUrl.com/auth-response")??false) {
// get your token from url
RegExp regExp = RegExp("access_token=(.*)");
String? token = regExp.firstMatch(url?.toString()??'')?.group(1);
print(token);
// or using CookieManager
CookieManager cookieManager = CookieManager.instance();
Cookie? cookie = await cookieManager.getCookie(
url: Uri.parse("https://myUrl.com/auth-response"),
name: "access_token");
print(cookie?.value??'');
// or using javascript to get access_token from localStorage
String? tokenFromJSEvaluation = await controller.evaluateJavascript(source: "localStorage.getItem('access_token')");
print(tokenFromJSEvaluation);
}
},
)
),
),
]))
);
}
}

For my case, I get my data via this -
InAppWebView(
initialUrlRequest: URLRequest(url: Uri.parse(widget.url)),
initialOptions: InAppWebViewGroupOptions(
crossPlatform: InAppWebViewOptions(userAgent: "random"),
//Add 'random' to 'userAgent' other wise you get error in webview for google signin.
android: AndroidInAppWebViewOptions(
domStorageEnabled: true,
databaseEnabled: true,
),
),
onWebViewCreated: (InAppWebViewController controller) {
webView = controller;
showAnimationLoader(context);
},
onLoadStop: (InAppWebViewController controller, Uri? url) async {
Get.back();
if (url?.toString().contains("my.domain.app") ?? false) {
debuggingPrint(
message: 'Code : ${url?.queryParameters['code'] ?? ''}');
debuggingPrint(
message: 'State : ${url?.queryParameters['state'] ?? ''}');
}
},
)

Related

How to create an Authentication middleware for a Flutter app?

This is my home.dart code and I want to write an Authentication Middleware for my app. At the moment my main.dart code looks like this:
void main() {
Get.put(MenuController());
Get.put(NavigationController());
Get.put(AuthController());
Get.put(AuthCard);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Obx(() => GetMaterialApp(
initialRoute: AuthController.instance.isAuth
? homeScreenRoute
: authenticationScreenRoute,
unknownRoute: GetPage(
name: '/not-found',
page: () => PageNotFound(),
transition: Transition.fadeIn),
getPages: [
GetPage(
name: rootRoute,
page: () {
return SiteLayout();
}),
GetPage(
name: authenticationScreenRoute,
page: () => const AuthenticationScreen()),
GetPage(name: homeScreenRoute, page: () => const HomeScreen()),
],
debugShowCheckedModeBanner: false,
title: 'BasicCode',
theme: ThemeData(
scaffoldBackgroundColor: light,
textTheme: GoogleFonts.mulishTextTheme(Theme.of(context).textTheme)
.apply(bodyColor: Colors.black),
pageTransitionsTheme: const PageTransitionsTheme(builders: {
TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
}),
primarySwatch: Colors.blue,
),
));
}
}
And the isAuth variable that I checking at the initialRoute part of the code comes from the following line of codes, inside the auth_controller file that extends GetXController:
final _isAuth = false.obs;
bool get isAuth {
_isAuth.value= token != null;
return _isAuth.value;
}
String? get token {
if (_expiryDate != null &&
_expiryDate!.isAfter(DateTime.now()) &&
_token != null) {
return _token;
}
return null;
}
Everything seems good but the application sticks at the authentication page and won't move to home screen after the isAuth's value changed to true!
I searched for that and found another way by creating an authentication middleware. So I added the following code bellow the above code inside the main.dart file:
class AuthMiddlware extends GetMiddleware {
#override
RouteSettings? redirect(String route) => !AuthController.instance.isAuth
? const RouteSettings(name: authenticationScreenRoute)
: null;
}
But I get a red line under the redirect word with no error decription and don't know how to complete the middleware and make it work?
Example of how to implement an AuthGuard with FirebaseAuth and Getx.
(If not using FirebaseAuth, swap it to your preferred authentication provider in AuthGuardMiddleware.)
middleware.dart
import 'auth.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AuthGuardMiddleware extends GetMiddleware {
var authService = Get.find<AuthService>();
#override
RouteSettings? redirect(String? route) {
return authService.isLoggedIn() ? null : RouteSettings(name: '/login');
}
}
auth.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:get/get.dart';
class AuthService extends GetxService {
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
bool isLoggedIn() {
return _firebaseAuth.currentUser != null;
}
// IMPLEMENT additional FirebaseAuth methods here.
}
main.dart
import 'package:get/get.dart';
import 'middleware.dart';
...
GetPage(
name: '/protected',
page: () => Protected()),
middlewares: [
AuthGuardMiddleware(),
]),
...
Copy paste :)
class AuthMiddlware extends GetMiddleware {
#override
RouteSettings? redirect(String? route) => !AuthController.instance.isAuth
? const RouteSettings(name: authenticationScreenRoute)
: null;
}

Flutter ListView of data from API does not show on my web app

Hello i try to create a listView of element from data of an API for my flutter web project. I get no result showing on the page each time i try. I was supposed to show a ListView of French department; I tried to use a code more basic for desperately trying to have results on my page. So I try to use a more simple code than mine to see how it work but still have no result(Blank page under appBar). I don't think I understand really well how that's supposed to worked but I find out a tutorial that had a sample code of a random ListView of element from data of an API; but this also don't work I'm a little bit stressed out... Is there a problem with this code ? Can I have more explanation about how to get data from an API and display them on a ListView on Flutter? Thank you everyone for the help
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
const baseUrl = "https://jsonplaceholder.typicode.com";
class API {
static Future getUsers() {
var url = baseUrl + "/users";
return http.get(url);
}
}
class User {
int id;
String name;
String email;
User(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
User.fromJson(Map json)
: id = json['id'],
name = json['name'],
email = json['email'];
Map toJson() {
return {'id': id, 'name': name, 'email': email};
}
}
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
build(context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'My Http App',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyListScreen(),
);
}
}
class MyListScreen extends StatefulWidget {
#override
createState() => _MyListScreenState();
}
class _MyListScreenState extends State {
var users = new List<User>();
_getUsers() {
API.getUsers().then((response) {
setState(() {
Iterable list = json.decode(response.body);
users = list.map((model) => User.fromJson(model)).toList();
});
});
}
initState() {
super.initState();
_getUsers();
}
dispose() {
super.dispose();
}
#override
build(context) {
return Scaffold(
appBar: AppBar(
title: Text("User List"),
),
body: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(title: Text(users[index].name));
},
));
}
}
Try using async and await on getUsers:
static Future getUsers() async {
var url = baseUrl + "/users";
return await http.get(url);
}
I've tested your code and everything is okay, you just need to add a ProgressIndicator.
Things to note:
Show a CircularProgressIndicator when making API calls
Separate your model class and API calls classes (put them in different files for quick
future modifications)
Use FutureBuilder for tasks than take long to execute (async tasks)
For your current implementation, modify as follows:
Initialize loading
class _MyListScreenState extends State {
var users = new List();
bool _isLoading = true;
2.Update state when loading is complete
_getUsers() {
API.getUsers().then((response) {
setState(() {
_isLoading = false;
Iterable list = json.decode(response.body);
users = list.map((model) => User.fromJson(model)).toList();
});
});
}
3. Add widget in your build method
body: _isLoading
? Center(child: CircularProgressIndicator())
: ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
return ListTile(title: Text(users[index].name));
},
));

Flutter crash when theres no internet

my app (literally took it from here) crashes whenever i disable internet connection, works like a charm with internet. how can i still able to access the page, displaying the last accessed result without getting stuck? i have all the internet permission included in manifest.
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
this is the code
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<Album> fetchAlbum() async {
final response =
await http.get('https://jsonplaceholder.typicode.com/albums/1');
if (response.statusCode == 200) {
// If the server did return a 200 OK response, then parse the JSON.
return Album.fromJson(json.decode(response.body));
} else {
// If the server did not return a 200 OK response, then throw an exception.
throw Exception('Failed to load album');
}
}
class Album {
final int userId;
final int id;
final String title;
Album({this.userId, this.id, this.title});
factory Album.fromJson(Map<String, dynamic> json) {
return Album(
userId: json['userId'],
id: json['id'],
title: json['title'],
);
}
}
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
MyApp({Key key}) : super(key: key);
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
Future<Album> futureAlbum;
#override
void initState() {
super.initState();
futureAlbum = fetchAlbum();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fetch Data Example',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Fetch Data Example'),
),
body: Center(
child: FutureBuilder<Album>(
future: futureAlbum,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data.title);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
),
),
),
);
}
}
Thank you
You can use connectivity library to listen for wifi status
import 'package:connectivity/connectivity.dart';
#override
initState() {
super.initState();
subscription = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
// Got a new connectivity status!
})
}
// Be sure to cancel subscription after you are done
#override
dispose() {
super.dispose();
subscription.cancel();
}
Use the Connectivity library to check network status, before making any HTTP requests.
The simplest solution, with no additional packages, is to put the code that connects to the internet in try/catch block. You must return a default object of Album, an empty list [] in case there is a list of Albums, in the catch block or throw an exception.
In your case, the code can be like this:
Future<Album> fetchAlbum() async {
try{
final response = await http.get('https://jsonplaceholder.typicode.com/albums/1');
if (response.statusCode == 200) {
// If the server did return a 200 OK response, then parse the JSON.
return Album.fromJson(json.decode(response.body));
} else {
// If the server did not return a 200 OK response, then throw an exception.
throw Exception('Failed to load album');
}
} catch(_){
print("No Internet Connection");
return Album(userId=-1, id=-1, title=''});
}
You can show a message to the user indicating the problem. You can do it like this:
showDialog(
context: context,
builder: (_) => AlertDialog(
title: Text('No Internet Connection',
style: TextStyle(
color: Colors.red, fontWeight: FontWeight.bold)),
content: Text('No Internet Connection. Please connect to the internet and try again'),
),
barrierDismissible: true);

How to wait for Future method inside widget testing?

I want to test my widget. In my HomePage widget, there's a method to call API then it will show the result inside this widget. Here's the code for HomePage widget
class HomePage extends StatefulWidget {
WebScraper _webScraper = WebScraper();
HomePage();
HomePage.forTest(Client client) {
_webScraper.client = client;
}
#override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<Post> posts = [];
bool isPostLoaded = false;
#override
void initState() {
super.initState();
_onRefresh();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Expanded(
child: RefreshIndicator(
child: isPostLoaded ? ListView.builder(
key: Key('post-list'),
itemCount: posts.length,
itemBuilder: (context, index) {
return Card(
key: Key(posts[index].id.toString()),
child: Column(
children: <Widget>[
Text(posts[index].title),
Text(posts[index].content),
Row(
children: <Widget>[
// some code
],
)
],
),
);
},
) : CircularProgressIndicator(),
onRefresh: _onRefresh),
),
],
),
);
}
Future<void> _onRefresh() async {
List<Post> postFromWebsite =
await this.widget._webScraper.getPostsFromWebsite();
if (postFromWebsite.length > 0) {
setState(() {
posts = postFromWebsite;
isPostLoaded = true;
});
}
}
And here is my test code
void main() {
var homeHttpMock;
setUp(() {
MockClient client = MockClient((request) async {
String html =
await rootBundle.loadString('test_resources/test.html');
return Response(html, 200);
});
homeHttpMock = MediaQuery(
data: MediaQueryData(),
child: MaterialApp(
home: HomePage.forTest(client),
),
);
});
testWidgets('Show post inside card in the list view',
(WidgetTester tester) async {
await tester.pumpWidget(homeHttpMock);
await tester.pump();
final listView = find.byKey(Key('post-list'));
expect(listView, findsOneWidget);
});
I tried to use tester.runAsync, tester.pump with duration, tester.pumpAndSettle (this one will be timed out), and FakeAsync but these methods don't work for my widget test and it will lead my test to be failed.
Thank you in advance
I found the solution after refactoring my test code using #LgFranco's advice and reading carefully in this article.
I changed my test code to this
testWidgets('Show post inside card in the list view',
(WidgetTester tester) async {
await tester.pumpWidget(homeHttpMock);
await tester.pumpAndSettle(); // Wait for refresh indicator to stop spinning
final listView = find.byKey(Key('post-list'));
expect(listView, findsOneWidget);
});
And I have to run it using flutter run my_test_code.dart command becuase if I didn't do that, it will raise pumpAndSettle timed out error. Then I realized that this is not a widget test because acording to the article I read, I can't test any real async work inside my widget test.
If anyone found another solution to this problem, please mention me. Thank you.
Why don't you just use await Future.delayed(Duration(seconds: 5));?
Just use the time that your fake post uses...

Accessing the value of a private variable in a method and making it global

I have a method that gets a token from a login, in a class that also returns the token. I need to access the token's value in other classes. i.e. It needs to be global.
The code works and I do get the correct value. I just need that value to be accessible globally.
class Home extends StatefulWidget {
#override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
return Scaffold(
appBar: new AppBarCall().getAppBar("Home"),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.search),
onPressed: () {
_oauth2();
},
),
drawer: new SideDrawer(),
body: HomePage(),
);
}
_oauth2() {
setState(() {
authenticate(values);
});
}
authenticate(...) async {
// login code
var token = tokenValue;
return token; // <---- this value needs to be global
}
}
var token;//now it is global, outside any class it the file
class _HomeState extends State<Home> {
return Scaffold(
//...
);
}
_oauth2() {
setState(() {
authenticate(values);
});
}
authenticate(...) async {
login code
token = tokenValue;//set the value here
return tokenValue;
}
}