pichillilorenzo / flutter_inappwebview

A Flutter plugin that allows you to add an inline webview, to use a headless webview, and to open an in-app browser window.
https://inappwebview.dev
Apache License 2.0
3.14k stars 1.47k forks source link

useShouldOverrideUrlLoading is true causes back behavior issues when webcontent contains redirects #1884

Open sofitry opened 9 months ago

sofitry commented 9 months ago

Environment

Technology Version
Flutter version 3.13.9
Plugin version 5.8.0
Android version api 34/Android 14
iOS version N/A
macOS version N/A
Xcode version N/A

Device information:

Description

When navigating to a webpage which contains a redirect using window.location.replace(...) the page remains in the history/back stack if useShouldOverrideUrlLoading is true. This causes serious problems when the page is simply a loading/routing page. In this situation, the user presses the back button, and is immediately returned forward again. In some cases double or triple clicking the back button quickly can overcome this, but from the user's perspective the back button is non-functional.

Functionally this is the same buggy behavior as: https://github.com/flutter/flutter/issues/137737 And this is technically a duplicate of: https://github.com/pichillilorenzo/flutter_inappwebview/issues/1732 with additional information and reproducible code.

Expected behavior: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace states that webpage which calls this should be removed from the history stack and not accessible by back navigation. Choosing to inspect a url and decide to overload or not should not affect basic browser functionality unless it is overridden.

Current behavior: If useShouldOverrideUrlLoading is true, regardless of the implementation of shouldOverrideUrlLoading even if it vacuously returns NavigationActionPolicy.ALLOW, the back stack will erroneously contain the webpage which called window.location.replace(nextpage).

Steps to reproduce

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

void main() {
  runApp(const MyApp());
}

const String pageWithAutoRedirect = '''
<!DOCTYPE html>
<html>
<head>
<title>Back button demo</title>
<script>
function myFunction() {
  setTimeout(function() { location.replace("https://developer.mozilla.org/en-US/docs/Web/API/Location/replace"); }, 1000);
}
</script>
</head>
<body onload=myFunction()>

<h1>This will auto-navigate in 1 second!</h1>
<p>If the webview functions correctly, this page should be removed from history</p>

</body>
</html>
''';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const Column(
        children: [
          Expanded(child: WebViewExample()),
          Divider(),
          Expanded(child: WebViewExample(implementOnNavigationRequest: true)),
        ],
      ),
    );
  }
}

class WebViewExample extends StatefulWidget {
  const WebViewExample({super.key, this.implementOnNavigationRequest = false});

  final bool implementOnNavigationRequest;

  @override
  State<WebViewExample> createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late final InAppWebViewController _controller;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      appBar: AppBar(
        title:
            Text(widget.implementOnNavigationRequest ? 'Incorrect' : 'Correct'),
      ),
      body: InAppWebView(
          key: UniqueKey(),
          initialData: InAppWebViewInitialData(data: pageWithAutoRedirect),
          initialOptions: InAppWebViewGroupOptions(
            crossPlatform: InAppWebViewOptions(
              javaScriptEnabled: true,
              useShouldOverrideUrlLoading: widget.implementOnNavigationRequest,
            ),
          ),
          onWebViewCreated: (controller) {
            _controller = controller;
          },
      ),
      floatingActionButton: backButton(),
    );
  }

  Widget backButton() {
    return FloatingActionButton(
      onPressed: () async {
        if (!await _controller.canGoBack()) {
          debugPrint('Cannot navigate back!');
        } else {
          _controller.goBack();
        }
      },
      child: const Icon(Icons.navigate_before),
    );
  }
}

Images

https://github.com/pichillilorenzo/flutter_inappwebview/assets/149631770/90f6fea5-9da8-48df-ab8c-88f7daa6139a

Stacktrace/Logcat

github-actions[bot] commented 9 months ago

👋 @sofitry

NOTE: This comment is auto-generated.

Are you sure you have already searched for the same problem?

Some people open new issues but they didn't search for something similar or for the same issue. Please, search for it using the GitHub issue search box or on the official inappwebview.dev website, or, also, using Google, StackOverflow, etc. before posting a new one. You may already find an answer to your problem!

If this is really a new issue, then thank you for raising it. I will investigate it and get back to you as soon as possible. Please, make sure you have given me as much context as possible! Also, if you didn't already, post a code example that can replicate this issue.

In the meantime, you can already search for some possible solutions online! Because this plugin uses native WebView, you can search online for the same issue adding android WebView [MY ERROR HERE] or ios WKWebView [MY ERROR HERE] keywords.

Following these steps can save you, me, and other people a lot of time, thanks!

jhpung commented 9 months ago

@pichillilorenzo Hello, lorenzo.

I think this issue ocurred by this parts. location.replace function is only replace current history of window. It doesn't add history. But when shoudlOverrideUrlLoading is true and return NavigationActionPolicy.ALLOW on shouldOverrideUrlLoading method, then webView.loadUrl is called.

https://github.com/pichillilorenzo/flutter_inappwebview/blob/2a4313dc50e21b71b4b8a2c45a5f5aa1657ae6b3/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java#L142-L154

https://github.com/pichillilorenzo/flutter_inappwebview/blob/2a4313dc50e21b71b4b8a2c45a5f5aa1657ae6b3/flutter_inappwebview_android/android/src/main/java/com/pichillilorenzo/flutter_inappwebview_android/webview/in_app_webview/InAppWebViewClient.java#L117-L128

So, when NavigationActionPolicy is 'ALLOW' then we should call WebViewClient's shoudlOverrideUrlLoading method

    return super.shouldOverrideUrlLoading(view, request);

But, I think this is breaking changes...

pichillilorenzo commented 9 months ago

@jhpung the WebViewClient.shouldOverrideUrlLoading native event is implemented to return a boolean in a synchronous way, that's impossible to handle on Flutter side. This is the only possible implementation for Android. iOS and macOS allows to handle that event asynchronously.

jhpung commented 9 months ago

@pichillilorenzo Thanks, lorenzo. that make sense...

sofitry commented 9 months ago

could a blocking adapter be made to work in android?

Like shouldOverrideUrlLoading calls a blocking function that starts a thread waiting for the message from Flutter? (probably with a timeout) Not sure how well the webview code would tolerate being blocked - if at all.

Alternatively - A feature request to allow passing some "rules" in that can be executed synchronously. Like a list of Patterns to be checked and if one matches, return ALLOW, otherwise BLOCK. (or something like that) would provide a reasonable workaround.

Second alternate - if there is any way to determine if the replace function was used, then either: remove it from the backstack manually, or provide a flag to shouldOverrideUrlLoading so the implementing code can attempt to handle it.

pichillilorenzo commented 9 months ago

shouldOverrideUrlLoading we can't block that method because it is running on UiThread unfortunately. I have already tried to do something like that in the past.

For subframe loading, you can use InAppWebViewSettings.regexToCancelSubFramesLoading, from the API Reference:

Regular expression used by [PlatformWebViewCreationParams.shouldOverrideUrlLoading] event to cancel navigation requests for frames that are not the main frame. If the url request of a subframe matches the regular expression, then the request of that subframe is canceled.

Officially Supported Platforms/Implementations:

  • Android native WebView

Probably, this could be something possible to implement:

Alternatively - A feature request to allow passing some "rules" in that can be executed synchronously. Like a list of Patterns to be checked and if one matches, return ALLOW, otherwise BLOCK. (or something like that) would provide a reasonable workaround.

By the way, I'm trying your example code with the latest plugin version 6 and it seems to work already fine. Plugin version 6 has changed quite a bit of things on the native platform side, so it seems that is already fixed. Consider that plugin version 5 won't be updated.

This is the updated example code to support the latest version 6:

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  runApp(const MyApp());
}

const String pageWithAutoRedirect = '''
<!DOCTYPE html>
<html>
<head>
<title>Back button demo</title>
<script>
function myFunction() {
  setTimeout(function() { location.replace("https://developer.mozilla.org/en-US/docs/Web/API/Location/replace"); }, 1000);
}
</script>
</head>
<body onload=myFunction()>

<h1>This will auto-navigate in 1 second!</h1>
<p>If the webview functions correctly, this page should be removed from history</p>

</body>
</html>
''';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: Column(
        children: [
          Expanded(child: WebViewExample()),
          Divider(),
          Expanded(child: WebViewExample(implementOnNavigationRequest: true)),
        ],
      ),
    );
  }
}

class WebViewExample extends StatefulWidget {
  const WebViewExample({super.key, this.implementOnNavigationRequest = false});

  final bool implementOnNavigationRequest;

  @override
  State<WebViewExample> createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late InAppWebViewController? _controller;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      appBar: AppBar(
        title:
            Text(widget.implementOnNavigationRequest ? 'Incorrect' : 'Correct'),
      ),
      body: InAppWebView(
        key: UniqueKey(),
        initialData: InAppWebViewInitialData(data: pageWithAutoRedirect),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          useShouldOverrideUrlLoading: widget.implementOnNavigationRequest,
        ),
        onWebViewCreated: (controller) {
          _controller = controller;
        },
      ),
      floatingActionButton: backButton(),
    );
  }

  Widget backButton() {
    return FloatingActionButton(
      onPressed: () async {
        final canGoBack = await _controller?.canGoBack() ?? false;
        if (!canGoBack) {
          debugPrint('Cannot navigate back!');
        } else {
          _controller?.goBack();
        }
      },
      child: const Icon(Icons.navigate_before),
    );
  }

  @override
  void dispose() {
    _controller = null;
    super.dispose();
  }
}

Screenshot: Screenshot 2023-12-07 alle 00 27 21

Console logs:

Launching lib/main.dart on sdk gphone64 x86 64 in debug mode...
Running Gradle task 'assembleDebug'...
✓  Built build/app/outputs/flutter-apk/app-debug.apk.
Installing build/app/outputs/flutter-apk/app-debug.apk...
Debug service listening on ws://127.0.0.1:50141/sD3_LPh9eHo=/ws
Syncing files to device sdk gphone64 x86 64...
W/ziparchive(10830): Unable to open '/data/app/~~jFLGCCN07QmCpb5O4Y83TQ==/com.google.android.trichromelibrary_604506638-9EXXktfUCP0f3a0kfvUmkw==/base.dm': No such file or directory
W/ziparchive(10830): Unable to open '/data/app/~~jFLGCCN07QmCpb5O4Y83TQ==/com.google.android.trichromelibrary_604506638-9EXXktfUCP0f3a0kfvUmkw==/base.dm': No such file or directory
W/pwebviewexample(10830): Entry not found
D/nativeloader(10830): Configuring clns-7 for other apk /data/app/~~jFLGCCN07QmCpb5O4Y83TQ==/com.google.android.trichromelibrary_604506638-9EXXktfUCP0f3a0kfvUmkw==/base.apk. target_sdk_version=34, uses_libraries=ALL, library_path=/data/app/~~9F9kSkEBEvVgdmCwTD5dIw==/com.google.android.webview-Yo8awwv35yYXYMcGrNV9NA==/lib/x86_64:/data/app/~~9F9kSkEBEvVgdmCwTD5dIw==/com.google.android.webview-Yo8awwv35yYXYMcGrNV9NA==/base.apk!/lib/x86_64:/data/app/~~jFLGCCN07QmCpb5O4Y83TQ==/com.google.android.trichromelibrary_604506638-9EXXktfUCP0f3a0kfvUmkw==/base.apk!/lib/x86_64, permitted_path=/data:/mnt/expand
D/nativeloader(10830): Configuring clns-8 for other apk /data/app/~~9F9kSkEBEvVgdmCwTD5dIw==/com.google.android.webview-Yo8awwv35yYXYMcGrNV9NA==/base.apk. target_sdk_version=34, uses_libraries=, library_path=/data/app/~~9F9kSkEBEvVgdmCwTD5dIw==/com.google.android.webview-Yo8awwv35yYXYMcGrNV9NA==/lib/x86_64:/data/app/~~9F9kSkEBEvVgdmCwTD5dIw==/com.google.android.webview-Yo8awwv35yYXYMcGrNV9NA==/base.apk!/lib/x86_64:/data/app/~~jFLGCCN07QmCpb5O4Y83TQ==/com.google.android.trichromelibrary_604506638-9EXXktfUCP0f3a0kfvUmkw==/base.apk!/lib/x86_64, permitted_path=/data:/mnt/expand
I/WebViewFactory(10830): Loading com.google.android.webview version 119.0.6045.66 (code 604506638)
I/cr_WVCFactoryProvider(10830): Loaded version=119.0.6045.66 minSdkVersion=29 isBundle=true multiprocess=true packageId=2
E/chromium(10830): [1207/002933.409404:ERROR:variations_seed_loader.cc(37)] Seed missing signature.
E/chromium(10830): [1207/002933.412846:ERROR:variations_seed_loader.cc(69)] Failed to open file for reading. Errno: 2
I/cr_LibraryLoader(10830): Successfully loaded native library
I/cr_CachingUmaRecorder(10830): Flushed 7 samples from 7 histograms, 0 samples were dropped.
I/cr_CombinedPProvider(10830): #registerProvider() provider:WV.w7@f8dfb1a isPolicyCacheEnabled:false policyProvidersSize:0
I/cr_PolicyProvider(10830): #setManagerAndSource() 0
I/cr_CombinedPProvider(10830): #linkNativeInternal() 1
D/CompatibilityChangeReporter(10830): Compat change id reported: 183155436; UID 10191; state: ENABLED
I/cr_AppResProvider(10830): #getApplicationRestrictionsFromUserManager() Bundle[EMPTY_PARCEL]
I/cr_PolicyProvider(10830): #notifySettingsAvailable() 0
I/cr_CombinedPProvider(10830): #onSettingsAvailable() 0
I/cr_CombinedPProvider(10830): #flushPolicies()
D/CompatibilityChangeReporter(10830): Compat change id reported: 214741472; UID 10191; state: ENABLED
D/CompatibilityChangeReporter(10830): Compat change id reported: 171228096; UID 10191; state: ENABLED
D/InAppWebView(10830): Using InAppWebViewClientCompat implementation
W/cr_WebSettings(10830): setForceDark() is a no-op in an app with targetSdkVersion>=T
W/cr_SupportWebSettings(10830): setForceDarkBehavior() is a no-op in an app with targetSdkVersion>=T
W/cr_media(10830): BLUETOOTH_CONNECT permission is missing.
W/cr_media(10830): registerBluetoothIntentsIfNeeded: Requires BLUETOOTH permission
I/PlatformViewsController(10830): Using hybrid composition for platform view: 0
D/InAppWebView(10830): Using InAppWebViewClientCompat implementation
W/cr_WebSettings(10830): setForceDark() is a no-op in an app with targetSdkVersion>=T
W/cr_SupportWebSettings(10830): setForceDarkBehavior() is a no-op in an app with targetSdkVersion>=T
I/PlatformViewsController(10830): Using hybrid composition for platform view: 1
[AndroidInAppWebViewWidget] (android) AndroidInAppWebViewWidget ID 0 calling "onWebViewCreated" using []
[AndroidInAppWebViewWidget] (android) AndroidInAppWebViewWidget ID 1 calling "onWebViewCreated" using []
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 10}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 10}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onLoadStart" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onUpdateVisitedHistory" using {isReload: false, url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 70}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onLoadStart" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onUpdateVisitedHistory" using {isReload: false, url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 70}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onTitleChanged" using {title: Back button demo}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onTitleChanged" using {title: Back button demo}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 80}
D/CompatibilityChangeReporter(10830): Compat change id reported: 236825255; UID 10191; state: ENABLED
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 100}
D/CompatibilityChangeReporter(10830): Compat change id reported: 193247900; UID 10191; state: ENABLED
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onLoadStop" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onPageCommitVisible" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 80}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onZoomScaleChanged" using {oldScale: 2.625, newScale: 0.65625}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onLoadStop" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onPageCommitVisible" using {url: about:blank}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onZoomScaleChanged" using {oldScale: 2.625, newScale: 0.65625}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "shouldOverrideUrlLoading" using {request: {headers: null, method: GET, networkServiceType: null, allowsConstrainedNetworkAccess: null, cachePolicy: null, body: null, url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace, allowsExpensiveNetworkAccess: null, attribution: null, assumesHTTP3Capable: null, httpShouldUsePipelining: null, allowsCellularAccess: null, httpShouldHandleCookies: null, timeoutInterval: null, mainDocumentURL: null}, sourceFrame: null, isRedirect: false, targetFrame: null, hasGesture: false, shouldPerformDownload: null, isForMainFrame: true, navigationType: null}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 10}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onZoomScaleChanged" using {oldScale: 0.65625, newScale: 1.1042962074279785}
[AndroidInAppWebViewController] (android) WebView ID 1 calling "onZoomScaleChanged" using {oldScale: 0.65625, newScale: 1.1042962074279785}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 10}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onLoadStart" using {url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onUpdateVisitedHistory" using {isReload: false, url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onTitleChanged" using {title: Location: replace() method - Web APIs | MDN}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 70}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onPageCommitVisible" using {url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onZoomScaleChanged" using {oldScale: 1.1042962074279785, newScale: 2.625}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 70}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 80}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 100}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onUpdateVisitedHistory" using {isReload: false, url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onLoadStop" using {url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onConsoleMessage" using {messageLevel: 0, message: (Glean.core.metrics.String) navigator.user_agent: Value length 171 exceeds maximum of 100.}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onReceivedTouchIconUrl" using {precomposed: false, url: https://developer.mozilla.org/apple-touch-icon.6803c6f0.png}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onProgressChanged" using {progress: 100}
D/EGL_emulation(10830): app_time_stats: avg=177.12ms min=5.51ms max=709.84ms count=7
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onLoadStop" using {url: https://developer.mozilla.org/en-US/docs/Web/API/Location/replace}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onConsoleMessage" using {messageLevel: 0, message: (Glean.core.metrics.String) navigator.user_agent: Value length 171 exceeds maximum of 100.}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onConsoleMessage" using {messageLevel: 1, message: (Glean.core.Upload.PingUploadManager) Ping 67c4985a-b795-4795-9885-bd31ec728650 successfully sent 200.}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onConsoleMessage" using {messageLevel: 1, message: (Glean.core.Upload.PingUploadManager) Ping bbcfbeda-0451-4ed0-b104-c13b8b55044d successfully sent 200.}
[AndroidInAppWebViewController] (android) WebView ID 0 calling "onConsoleMessage" using {messageLevel: 1, message: (Glean.core.Upload.PingUploadManager) Ping 1f98501e-db88-48a4-8d25-a7d927c0b39a successfully sent 200.}
D/CompatibilityChangeReporter(10830): Compat change id reported: 289878283; UID 10191; state: ENABLED
D/EGL_emulation(10830): app_time_stats: avg=1008.71ms min=2.70ms max=23970.52ms count=24
sofitry commented 8 months ago

sadly version 6 does not solve the problem after the code has been adjusted:

import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';

void main() {
  runApp(const MyApp());
}

const String pageWithAutoRedirect = '''
<!DOCTYPE html>
<html>
<head>
<title>Back button demo</title>
<script>
function myFunction() {
  setTimeout(function() { location.replace("https://developer.mozilla.org/en-US/docs/Web/API/Location/replace"); }, 1000);
}
</script>
</head>
<body onload=myFunction()>

<h1>This will auto-navigate in 1 second!</h1>
<p>If the webview functions correctly, this page should be removed from history</p>

</body>
</html>
''';

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const Column(
        children: [
          Expanded(child: WebViewExample()),
          Divider(),
          Expanded(child: WebViewExample(implementOnNavigationRequest: true)),
        ],
      ),
    );
  }
}

class WebViewExample extends StatefulWidget {
  const WebViewExample({super.key, this.implementOnNavigationRequest = false});

  final bool implementOnNavigationRequest;

  @override
  State<WebViewExample> createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late final InAppWebViewController _controller;

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.green,
      appBar: AppBar(
        title:
            Text(widget.implementOnNavigationRequest ? 'Incorrect' : 'Correct'),
      ),
      body: InAppWebView(
        key: UniqueKey(),
        initialData: InAppWebViewInitialData(data: pageWithAutoRedirect),
        initialSettings: InAppWebViewSettings(
          javaScriptEnabled: true,
          useShouldOverrideUrlLoading: widget.implementOnNavigationRequest,
        ),
        onWebViewCreated: (controller) {
          _controller = controller;
        },
        // Version 6 appears to require providing an implementation of shouldOverrideUrlLoading if useShouldOverrideUrlLoading is true. Otherwise it behaves as if navigation were canceled.
        shouldOverrideUrlLoading: (controller, navAction) async {
          return NavigationActionPolicy.ALLOW;
        },
      ),
      floatingActionButton: backButton(),
    );
  }

  Widget backButton() {
    return FloatingActionButton(
      onPressed: () async {
        if (!await _controller.canGoBack()) {
          debugPrint('Cannot navigate back!');
        } else {
          _controller.goBack();
        }
      },
      child: const Icon(Icons.navigate_before),
    );
  }
}
HanCharles commented 8 months ago

I'm facing the same issue. Even after trying the latest version, 6.0.0, the problem persists.

For instance, when navigating from the home screen (index.php) to a specific page (myp_main.php) that requires a session, if the session is not present on that specific page, it redirects to the login page (login.php) using location.href. However, upon pressing the back button or using history.back() on the login page, it goes back to the specific page again, leading to a situation where there's no session, and it redirects back to the login page.

On other browsers like Chrome and Safari, this website behaves correctly, going back to the previous page (index.php) when the back button is pressed. I tested the iOS app with version 6.0.0, and the back button worked correctly.

pichillilorenzo commented 8 months ago

@HanCharles what is the code you are using?

HanCharles commented 8 months ago

@pichillilorenzo index.php

<a href="/myp_main.php">my page</a>

myp_main.php

<?
if(!$_SESSION) {
$returnUrl = $_SERVER['SCRIPT_NAME'] . ($_SERVER['QUERY_STRING'] ? '?' . $_SERVER['QUERY_STRING'] : '');
?>
<script>
top.location.href="/login.php?returnUrl='.urlencode($returnUrl)";
</script>
<?
}
// ... myp_main
?>

login.php

<?
// ... login input 
?>
import 'dart:async';
import 'dart:io';

import 'package:cornermarket_flutter/constant/constants.dart';
import 'package:cornermarket_flutter/controller/notification_controller.dart';
import 'package:cornermarket_flutter/widget/loader.dart';
import 'package:facebook_app_events/facebook_app_events.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';

class WebViewController extends StatefulWidget {
  final String userAgent;
  final FirebaseAnalytics analytics;
  final FacebookAppEvents facebookAppEvents;

  const WebViewController({Key? key, required this.userAgent, required this.analytics, required this.facebookAppEvents}) : super(key: key);

  @override
  State<WebViewController> createState() => _WebViewControllerState();
}

class _WebViewControllerState extends State<WebViewController> {

  static const platform = MethodChannel('intent');

  final GlobalKey webViewKey = GlobalKey();

  InAppWebViewController? webViewController;
  late InAppWebViewGroupOptions options;
  late NotificationController notificationController;
  late PullToRefreshController pullToRefreshController;
  // String url = "";
  double progress = 0;

  String deviceToken = "";
  // final urlController = TextEditingController();

  WebHistory? webHistory;

  @override
  void initState() {
    super.initState();
    print('initState ------------------------');

    print('widget.userAgent : ${widget.userAgent}');

    InAppWebViewGroupOptions _options = InAppWebViewGroupOptions(
        crossPlatform: InAppWebViewOptions(
          javaScriptEnabled: true,
          javaScriptCanOpenWindowsAutomatically: true,
          useShouldOverrideUrlLoading: true,
          mediaPlaybackRequiresUserGesture: false,
          userAgent: widget.userAgent,
          cacheEnabled: true,
        ),
        android: AndroidInAppWebViewOptions(
          useHybridComposition: true,
          hardwareAcceleration: true,
        ),
        ios: IOSInAppWebViewOptions(
          allowsInlineMediaPlayback: true,
        )
    );
    options = _options;

    pullToRefreshController = PullToRefreshController(
      options: PullToRefreshOptions(
        color: Colors.blue,
      ),
      onRefresh: () async {
        if (Platform.isAndroid) {
          webViewController?.reload();
        } else if (Platform.isIOS) {
          webViewController?.loadUrl(
              urlRequest: URLRequest(url: await webViewController?.getUrl()));
        }
      },
    );

    initialization();
  }

  void initialization() async {
    print('ready in 3...');
    await Future.delayed(const Duration(seconds: 1));
    print('ready in 2...');
    await Future.delayed(const Duration(seconds: 1));
    print('ready in 1...');
    await Future.delayed(const Duration(seconds: 1));
    print('go!');
    FlutterNativeSplash.remove();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: WillPopScope(
          onWillPop: () => _goBack(context),
          child :Stack(
            children: [
              InAppWebView(
                key: webViewKey,
                initialUrlRequest: URLRequest(
                    url: "/index.php",
                    headers: {}
                ),
                initialOptions: options,
                pullToRefreshController: pullToRefreshController,
                onWebViewCreated: (controller) {
                  webViewController = controller;
                },
                onLoadStart: (controller, url) async {
                  setState(() {
                    // this.url = url.toString();
                    // urlController.text = this.url;
                  });
                },
                androidOnPermissionRequest: (controller, origin, resources) async {
                  return PermissionRequestResponse(
                      resources: resources,
                      action: PermissionRequestResponseAction.GRANT);
                },
                shouldOverrideUrlLoading: (controller, navigationAction) async {
                  var uri = navigationAction.request.url!;

                  if (![ "http", "https", "file", "chrome", "data", "javascript", "about"].contains(uri.scheme)) {
                    uri.forceToStringRawValue = true;
                    if(Platform.isAndroid) {
                      if (await canLaunchUrl(uri)) {
                        await launchUrl(uri,);
                        return NavigationActionPolicy.CANCEL;
                      } else {
                        try {
                          var result = await platform.invokeMethod('getAppUrl', {'url': uri.rawValue});
                          if(result != null) {
                            await controller.loadUrl(urlRequest: URLRequest(url: WebUri(result)));
                          }
                        } catch(e) {
                          print('url fail $e');
                        }
                        return NavigationActionPolicy.CANCEL;
                      }
                    } else if(Platform.isIOS){
                      if (await canLaunchUrl(uri)) {
                        await launchUrl(uri,);
                        return NavigationActionPolicy.CANCEL;
                      }
                    }
                  } else {
                    if (Platform.isAndroid || navigationAction.iosWKNavigationType == IOSWKNavigationType.LINK_ACTIVATED) {
                        controller.loadUrl(urlRequest: URLRequest(url: uri));
                      return NavigationActionPolicy.CANCEL;
                    }
                  }

                  return NavigationActionPolicy.ALLOW;
                },
                onLoadStop: (controller, url) async {
                  pullToRefreshController.endRefreshing();
                  setState(() {
                    // this.url = url.toString();
                    // urlController.text = this.url;
                  });
                },
                onLoadError: (controller, url, code, message) {
                  pullToRefreshController.endRefreshing();
                },
                onProgressChanged: (controller, progress) {
                  if (progress == 100) {
                    pullToRefreshController.endRefreshing();
                  }
                  setState(() {
                    this.progress = progress / 100;
                    // urlController.text = this.url;
                  });
                },
                onUpdateVisitedHistory: (controller, url, androidIsReload) async {
                  print("Visited: $url");
                  webHistory = await controller.getCopyBackForwardList();
                  // if(webHistory!.list!.last.url!.path.contains("login.php")) {
                  //   controller.goBack();
                  // }
                  setState(() {
                    // this.url = url.toString();
                    // urlController.text = this.url;
                  });
                },
                onConsoleMessage: (controller, consoleMessage) {
                  print('consoleMessage : $consoleMessage');
                },
              ),
              progress < 1.0 ? Loader() : Container(),
            ],
          ),
        ),
      ),
    );
  }

  Future<bool> _goBack(BuildContext context) async{
    if(await webViewController!.canGoBack()){
      webViewController!.goBack();
      return Future.value(false);
    }else{
      return Future.value(true);
    }
  }

  @override
  void dispose() {
    // TODO: implement dispose
    webViewController = null;
    super.dispose();
  }
}

The code above is a condensed form provided for explanation purposes. Was there an issue in the code?

There doesn't seem to be a problem on Flutter iOS or the web, but it arises only on Android. In an attempt to address this issue, I tried implementing the following code within the shouldOverrideUrlLoading function, but it didn't resolve the problem :(

                  if (Platform.isAndroid) {
                    print("WebHistory : ${webHistory}");
                    print("webHistory!.list!.length : ${webHistory!.list!.length}");
                    print("webHistory!.currentIndex : ${webHistory!.currentIndex}");
                    if (webHistory!.list!.length > 2 &&
                        // webHistory!.list!.length-1 > webHistory!.currentIndex) &&
                        webHistory!.list!.length-2 == webHistory!.currentIndex! &&
                        webHistory!.list!.last.url.toString().contains("login.php")) {
                      controller.loadUrl(urlRequest: URLRequest(url: webHistory!.list!.elementAt(webHistory!.list!.length - 3).url));
                      return NavigationActionPolicy.CANCEL;
                    }
                  }
lyb5834 commented 7 months ago

is there any solutions? 😭

HanCharles commented 7 months ago

not yet

lyb5834 commented 7 months ago

@pichillilorenzo Hi, i soluted this issue by add a setting regexToCancelOverrideUrlLoading for exp: regexToCancelOverrideUrlLoading: '^(?!http://|https://).*' , it worked for me!!

pichillilorenzo commented 7 months ago

@lyb5834 thanks for the PR, I was already planning to add something like that. It should be almost ok. I will review the code changes and merge it as soon as possible!

carlos-dev-yang commented 4 months ago

any updates?