How to implement reCaptcha into a flutter app - flutter

I am trying to implement the reCaptcha function to my flutter app, but in the captcha registration I need to provide a domain which I don't have one for a mobile app. I have browsed several guides teaching how to implement reCaptcha into a mobile app, yet those guides registered their reCaptcha with package names but not domains. What is the correct way to implement reCaptcha in a flutter app, or any mobile app in 2020?

You can use this plugin, flutter_recaptcha.
For the domain, I had the same issue. I first found that I needed to use the "I'm not a robot" checkbox option from here and I had to check the github repository to find this information, "!!! Remember to add this domain into the reCaptcha setting: recaptcha-flutter-plugin.firebaseapp.com," which explains it.
I was lost for a bit after not seeing that on the main page, but now it makes sense. Hopefully it helps.
Edit
I noticed something after trying it out, that I'd like to mention. The plugin does not provide a captcha response for using to authenticate the user server-side, so it does not seem very useful as it is. However, it is a simple plugin, so it may be possible to use it as an example. The steps, I think, would be to create a webpage with the captcha. As with the plugin, use a webview to open the page, then capture the post output of the form and ip address of user submitting the form, using something like this, then send it to flutter and then submit your request with that information, and use the Google library to verify the captcha.
Instructions
I just finished implementing this and I found a good way that works.
First, create an html page, like this:
<html>
<head>
<title>reCAPTCHA</title>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>
</head>
<body style='background-color: aqua;'>
<div style='height: 60px;'></div>
<form action="?" method="POST">
<div class="g-recaptcha"
data-sitekey="YOUR-SITE-KEY"
data-callback="captchaCallback"></div>
</form>
<script>
function captchaCallback(response){
//console.log(response);
if(typeof Captcha!=="undefined"){
Captcha.postMessage(response);
}
}
</script>
</body>
</html>
Then, host that on your domain, say example.com/captcha.
Then, create a flutter Widget, like this:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
class Captcha extends StatefulWidget{
Function callback;
Captcha(this.callback);
#override
State<StatefulWidget> createState() {
return CaptchaState();
}
}
class CaptchaState extends State<Captcha>{
WebViewController webViewController;
#override
initState(){
super.initState();
}
#override
Widget build(BuildContext context) {
return Center(
child: WebView(
initialUrl: "https://example.com/captcha.html",
javascriptMode: JavascriptMode.unrestricted,
javascriptChannels: Set.from([
JavascriptChannel(
name: 'Captcha',
onMessageReceived: (JavascriptMessage message) {
//This is where you receive message from
//javascript code and handle in Flutter/Dart
//like here, the message is just being printed
//in Run/LogCat window of android studio
//print(message.message);
widget.callback(message.message);
Navigator.of(context).pop();
})
]),
onWebViewCreated: (WebViewController w) {
webViewController = w;
},
)
);
}
}
Make sure you registered for a captcha key at https://www.google.com/recaptcha (click on "Admin Console" at the top-right).
Then, you have the front-end built. To call a captcha, just run:
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return Captcha((String code)=>print("Code returned: "+code));
}
),
);
You can use whatever callback you want to, like this:
class GenericState extends State<Generic>{
void methodWithCaptcha(String captchaCode){
// Do something with captchaCode
}
#override
Widget build(BuildContext context) {
return Center(child:FlatButton(
child: Text("Click here!"),
onPressed: (){
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return Captcha(methodWithCaptcha);
}
),
);
}
}
}
Server-side, you can follow the instructions here (I followed the sections "Direct Download" and "Usage"). I found that for the usage, I could simply use the code:
$recaptcha = new \ReCaptcha\ReCaptcha($secret);
$resp = $recaptcha->verify($gRecaptchaResponse, $remoteIp);
if ($resp->isSuccess()) {
// Verified!
} else {
$errors = $resp->getErrorCodes();
}
Using setExpectedHostname, like in the example, was unnecessary.
After that, everything works! I think this is currently the best way to implement Google reCaptcha V2 in flutter (for both iOS and Android).

If you are looking for Flutter WEB, You can try g_recaptcha_v3 package
Note:
it supports reCAPTCHA V3 only and not V2
its for Flutter Web only and no other platform supports

I've improved #JVE999 approach and created a new package:
flutter_firebase_recaptcha
The package uses InAppWebView for recapcha HTML rendering, so you don't need separate web page anymore.
My widget supports 'visible' and 'invisible' recapcha. Invisible recapcha allows you try to get recapcha token without showing anything to your user.

Related

Prevent refresh button or prompt message if click refresh (flutter web)

i know that for back button we can do by using the willpopscope,
but right now i' m trying to prevent user from refreshing the page, is there a way to disable the refresh button or prompt a message if user click the refresh button?
example of willpopscope
return WillPopScope (
onWillPop: () async {
return shouldPop;
},
child: const Text('WillPopScope sample'),
);
Adding this to lib/web/index.html will display a confirmation dialog:
...
<body>
<script type="text/javascript">
window.onbeforeunload = function() {
return "Changes that you made may not be saved.";
}
</script>
...
</body>
...
Like so:
A more elegant solution for web would be:
import 'dart:html' as html;
html.window.onBeforeUnload.listen((event) async {
if (event is html.BeforeUnloadEvent) event.returnValue = "Don't go!";
});
Like this only the page where you want to warn, triggers the event. Otherwise all windows would trigger the event.

Flutter Web View blank screen on Release Mode

I have an issue where the Flutter WebView is rendering blank on the release version of my Android App on the Playstore.
Users are complaining that a page is not loading, but this page was loading very fine in debug mode.
Here is what I am doing. I am loading a local html file from my assets folder as shown in the code below:
import 'package:webview_flutter/webview_flutter.dart';
class _InvoicePageState extends ResumableState<InvoicePage>
with SingleTickerProviderStateMixin {
WebViewController? _webViewController;
#override
initState() {
super.initState();
}
void loadPage()async{
String html = await rootBundle.loadString('assets/html/invoice.html');
_webViewController?.loadUrl(Uri.dataFromString(html ,
mimeType: 'text/html', encoding: Encoding.getByName('utf-8'))
.toString());
}
//Then in my build Method I have this
....
WebView(
onWebViewCreated:
(WebViewController webViewController) {
_webViewController = webViewController;
loadPage();
},
),
What could be wrong, why is it loading in debug mode but not loading in release mode?
My users are already dropping really bad reviews.
Please help.
Thank you.
The library's official example has InAppWebView embedded inside Stack which is further embedded inside Expanded. That is causing the issue. Try removing the Stack and Expanded or if you really want Stack, then first embed InAppWebView inside Expanded and then remove the parent Expanded.

Is there any way to parse html created with javascript in flutter?

I would like to use metadata_fetch package to get the OGP.
The implementation of this package uses the html (parse) package. And it worked for most of the web pages.
However, there are some web pages, like this one, that cannot be fetched. I think this was because the html was generated by javascript. Is there any way to parse such a page as well?
You should be able to create a webview_flutter and inject some sort of Javascript that would walk the DOM for you and serialize it for return, which of course would happen after the page javascript has already built up the DOM. I haven't done that, but it might be a fun project.
EDIT: it might be as simple as capturing the string response of document.firstElementChild.outerHTML.
For the benefit of others, here's a source I made based on an idea from #Randal Schwartz.
const String url_unext = 'https://video.unext.jp/title/SID0050925';
WebViewController _controller;
Stack(
children: [
WebView(
onWebViewCreated: (controller) {
_controller = controller;
},
javascriptMode: JavascriptMode.unrestricted,
initialUrl: url_unext,
onPageFinished: (_) async {
html = await _controller.evaluateJavascript("window.document.firstElementChild.outerHTML;");
// Use Metadata_fetch to parse
final data = getOpenGraphDataFromResponse(html);
print(data);
},
),
// else widget here
Container(),
],
),
However, I am concerned about running a malicious script. If there is a better way, please let me know. I'm looking into the possibility of using oembed.
Once again, thank you very much, #Randal Schwartz.

How to stop Flutter Web HtmlElementView from rebuilding/rerunning?

I have built a video calling app in Flutter using the Agora SDK. This is only available for iOS/Android and so for the web build I had to build a wrapper around the existing Agora web SDK. Due to the way Flutter renders web elements inside a shadow DOM, you cannot access elements by document.getElementById(), which is what the Agora SDK uses to inject their video player. To get around this I am rendering an IFrame that has the div and Agora SDK script bundled together.
It is all working nicely but when any event is triggered inside the browser window, such as the mouse entering a button or clicking anything, the IFrame refreshes and rebuilds the video view which takes 1-2 seconds to initialize.
Is there anyway I can unlink the IFrame from the browser events? I tried marking the HtmlElementView as const and putting it inside a StatefulWidget that only registers the platform view once. The widget isn't actually running the build() method again but the IFrame still refreshes.
Flutter code
ui.platformViewRegistry.registerViewFactory(
'video-container',
(int viewId) => IFrameElement()
..id = 'my-iframe'
..width = '100%'
..height = '100%'
..src = 'assets/web_elements/agora_container.html'
..allow = "camera; microphone"
..style.border = 'none');
#override
Widget build(BuildContext context) {
print("*****\n\n\nBuilding the web host container\n\n\n*****"); // this is only printing once
return const HtmlElementView(
viewType: 'video-container',
);
}
Agora code
<div id="local-video"></div>
<div id="remote-video"></div>
<div id="video-canvas"></div>
<script src="scripts/AgoraSDK.js"></script>
<script src="scripts/agora_controller.js"></script>
What worked for me was this:
Key key = UniqueKey(); // before the build method
HtmlElementView(
key: key,
viewType: 'iframeElement',
);
Now there is no rebuilding/rerunning. Note I am using the iframeElement, not video-container, but this should work. Please comment.
This is one of the active problem with the Flutter Web. I guess a workaround to this is mentioned here: https://github.com/flutter/flutter/issues/53253#issuecomment-607197549

Navigating away when a Flutter FutureProvider resolves it's future

I'm basically looking for a way to either start loading data, or navigate to the Login Screen.
The FutureProvider gets it's value from SharedPreferences. The default homescreen is just a logo with a spinner.
If the userID resolves to null, the app should Navigate to the Login Screen, otherwise it should call a method that will start loading data and then on completion navigate to the main page.
Can this be achieved with FutureProvider?
I add it to the page build to ensure the page widget will subscribe to the Provider:
Widget build(BuildContext context) {
userInfo = Provider.of<UserInfo>(context);
print('Building with $userInfo');
return PageWithLoadingIndicator();
....
I added it to didChangeDependencies to react to the change:
#override
void didChangeDependencies() {
print('Deps changed: $userInfo');
super.didChangeDependencies();
// userInfo = Provider.of<UserInfo>(context); // Already building, can't do this.
// print('And now: $userInfo');
if (userInfo == null) return;
if (userInfo.userId != null) {
startUp(); // Run when user is logged in
} else {
tryLogin(); // Navigate to Login
}
}
And since I can't use Provider.of in initState I added a PostFrameCallback
void postFrame(BuildContext context) {
print('PostFrame...');
userInfo = Provider.of<UserInfo>(context);
}
Main is very simple - it just sets up the MultiProvider with a single FutureProvider for now.
class MyApp extends StatelessWidget {
Future<UserInfo> getUserInfo() async {
String token = await UserPrefs.token;
return UserInfo.fromToken(
token,
);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App One',
theme: ThemeData(
primarySwatch: Colors.purple,
),
home: MultiProvider(
providers: [FutureProvider<UserInfo>(builder: (_) => getUserInfo())],
child: LoadingScreen(),
),
);
}
}
The problem is that as it is now I can see from the print statements that "didChangeDependencies" gets called twice, but the value of userInfo is always null, even though build() eventually gets an instance of UserInfo as evident by the print statement in the build() method.
I'm guessing I can add some logic into the build method but that screams against my sensibilities as the wrong place to do it... perhaps it isn't as bad as I think?
I've decided that this is conceptually the wrong approach.
In my case I wanted to use a FutureProvider to take the results from an Async function which create a "Config" object using SharedPreferences. The FutureProvider would then allow the rest of the app to access the user's config settings obtained from sharepreferences.
This still feels to me like a valid approach. But there are problems with this from an app flow perspective.
Mainly that the values from the shared preferences includes the logged in user session token and username.
The app starts by showing a Loading screen with a Circular Progress bar. The app then reads the shared preferences and connects online to check that the session is valid. If there is no session, or if it is not valid, the app navigates to the Login "wizzard" which asks username, then on the next page for the password and then on the next page for 2-factor login. After that it navigates to the landing page. If the loading page found a valid session, the login wizzard is skipped.
The thing is that the two things - app state and app flow are tangenially different. The app flow can result in changes being store in the app state, but the app state should not affect the app flow, at least not in this way, conceptually.
In practical terms I don't think calling Navigator.push() from a FutureProvider's build function is valid, even if context is available. I could be wrong about this, but I felt the flowing approach is more Flutteronic.
#override
void initState() {
super.initState();
_loadSharedPrefs().then((_) {
if(this.session.isValid()) _navToLandingPage();
else _navToLoginStepOne();
}
}
I'm open to better suggestions / guidance