Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] shouldOverrideUrLoading issue with intercepting link to open a new window using url launcher #2089

Open
gd46 opened this issue Apr 3, 2024 · 3 comments
Labels
bug Something isn't working

Comments

@gd46
Copy link

gd46 commented Apr 3, 2024

Environment

Technology Version
Flutter version 3.13.8+
Plugin version 6.0.0
iOS version 17.x+
Xcode version 15.2
Url launcher 6.1.14

Issue was introduced on v6.0.0-beta.26 by this commit

e62348a

Last worked on v6.0.0-beta.25

Device information: iPhone 15

public func webView(_ webView: WKWebView,
                 decidePolicyFor navigationAction: WKNavigationAction,
                 decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
        var decisionHandlerCalled = false
        let callback = WebViewChannelDelegate.ShouldOverrideUrlLoadingCallback()
        callback.nonNullSuccess = { (response: WKNavigationActionPolicy) in
            decisionHandlerCalled = true
            decisionHandler(response)
            return false
        }
        callback.defaultBehaviour = { (response: WKNavigationActionPolicy?) in
            if !decisionHandlerCalled {
                decisionHandlerCalled = true
                decisionHandler(.allow)
            }
        }
        callback.error = { [weak callback] (code: String, message: String?, details: Any?) in
            print(code + ", " + (message ?? ""))
            callback?.defaultBehaviour(nil)
        }

        let runCallback = {
            if let useShouldOverrideUrlLoading = self.settings?.useShouldOverrideUrlLoading, useShouldOverrideUrlLoading, let channelDelegate = self.channelDelegate {
                channelDelegate.shouldOverrideUrlLoading(navigationAction: navigationAction, callback: callback)
            } else {
                callback.defaultBehaviour(nil)
            }
        }
        
        
        if windowId != nil, !windowCreated {
            windowBeforeCreatedCallbacks.append(runCallback)
        } else {
            runCallback()
        }
    }

When shouldOverrideUrlLoading intercepts a link to launch using url launcher package the windowId here is not always null and then never executes the runCallback causing the link to not open. In testing introducing a delay around the following piece caused the issue to happen more frequently:

 if windowId != nil, !windowCreated {
    windowBeforeCreatedCallbacks.append(runCallback)
} else {
     runCallback()
 }

Expected behavior:

shouldOverrideUrlLoading should execute every time so that the url launcher can open the link

Current behavior:

runCallback is never executed(because windowId is not null, and windowBeforeCreatedCallbacks adds the runCallback but is not executed until webview is destroyed). so the link does not open. The windowId gets removed / reset when the webview is destroyed.

Steps to reproduce

  1. Create a webview utilizing the shouldOverrideUrlLoading
  2. Intercept links to launch a url
  3. Open and close that link rapidly. If this doesn't work, use a page with two links and alternate between them.
    3.1. Another option is to update the runCallback check in a delay which increases the problem:
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
       if windowId != nil, !windowCreated {
            windowBeforeCreatedCallbacks.append(runCallback)
        } else {
            runCallback()
        }
}
@gd46 gd46 added the bug Something isn't working label Apr 3, 2024
Copy link

github-actions bot commented Apr 3, 2024

👋 @gd46

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!

@gd46
Copy link
Author

gd46 commented Sep 30, 2024

See below example of the issue.

Where you see UrlLauncherService see

import 'package:injectable/injectable.dart';
import 'package:url_launcher/url_launcher_string.dart';

@lazySingleton
class UrlLaunchService {
  Future<bool> launchUrl(String url) async {
    if (await canLaunchUrlString(url)) {
      return launchUrlString(url);
    } else {
      return Future.value(false);
    }
  }
}

This url launcher is another package that when we intercept a path in shouldOverrideUrlloading we tell it when to allow opening an external browser, or telephone functionality.

/// Webview options.
          final InAppWebViewSettings initialSettings = InAppWebViewSettings(
            isInspectable: state.appContextModel.showDebugConsole,
            allowFileAccessFromFileURLs: true,
            allowUniversalAccessFromFileURLs: true,
            useShouldOverrideUrlLoading: true,
            mediaPlaybackRequiresUserGesture: false,
            applicationNameForUserAgent: widget.applicationNameForUserAgent,
            transparentBackground: true,
            useHybridComposition: true,
            forceDark: ForceDark.AUTO,
            allowsInlineMediaPlayback: true,
            disallowOverScroll: widget.preventBounce,
            sharedCookiesEnabled: true,
            allowsBackForwardNavigationGestures: false,
            allowingReadAccessTo: widget.url == null &&
                    (widget.initialFile != null &&
                        widget.initialFile!.startsWith('file'))
                ? WebUri(widget.initialFile!)
                : null,
          );

          return MediaQuery.removePadding(
              context: context,
              removeTop: true,
              child: Scaffold(
                resizeToAvoidBottomInset:
                    getIt.get<PlatformService>().isAndroid,
                appBar: widget.title != null
                    ? SecondaryAppBarWidget(
                        title: widget.title!,
                        closeAction: () async {
                          await widget.webviewIntegrationService
                              .onLoadWebviewShellClosed();
                        },
                      )
                    : null,
                body: FutureBuilder(
                      future: _webviewBridgeJsFile,
                      builder: (BuildContext context,
                          AsyncSnapshot<String> snapshot) {
                        if (snapshot.connectionState == ConnectionState.done &&
                            snapshot.hasData) {
                          return InAppWebView(
                            key: _webViewKey,
                            initialUrlRequest: widget.url != null
                                ? URLRequest(
                                    url: WebUri(getIt
                                        .get<DebugManager>()
                                        .addDebugOverridesToUri(
                                          widget.url?.rawValue,
                                          appContextModel:
                                              state.appContextModel,
                                        )!),
                                  )
                                : (widget.initialFile != null &&
                                        widget.initialFile!.startsWith('file'))
                                    ? URLRequest(
                                        url: WebUri(widget.initialFile!))
                                    : null,
                            initialFile: (widget.initialFile != null &&
                                    !widget.initialFile!.startsWith('file'))
                                ? widget.initialFile
                                : null,                         
                            gestureRecognizers: gestureRecognizers,
                            initialUserScripts:
                                UnmodifiableListView<UserScript>(
                              [
                                UserScript(
                                  source: snapshot.data!,
                                  injectionTime:
                                      UserScriptInjectionTime.AT_DOCUMENT_START,
                                ),
                              ],
                            ),
                            initialSettings: initialSettings,
                            onWebViewCreated: (controller) async {
                              _webViewController = controller;

                              widget.webviewIntegrationService
                                  .setWebViewController(controller);

                              widget.webviewIntegrationService
                                  .injectJavaScriptHandlers(context);
                            },
                            shouldOverrideUrlLoading:
                                (controller, navigationAction) async {
                              _logger.info(
                              'shouldOverrideUrlLoading: $navigationAction',
                              stackTrace: StackTrace.current,
                            );
                            bool allowNavigation = false;
                            Uri? url = navigationAction.request.url;

                            if (url?.scheme == 'blob') {
                              _logger.info('download request.url $url', stackTrace: StackTrace.current);
                            } else if (url?.scheme == 'https') {
                              if (url != null &&
                                  (url.host == initialWebviewHost ||
                                      url.host == 'www.youtube.com' ||
                                      url.host.endsWith('.plaid.com') ||
                                      url.host.endsWith('.driftt.com'))) {
                                allowNavigation = true;
                              } else {
                                _logger.info('BLOCKED navigating within app, url: $url',
                                    stackTrace: StackTrace.current);
                                _urlLaunchService.launchUrl(url.toString());
                              }
                            } else if (url?.scheme == 'tel') {
                              _urlLaunchService.launchUrl(url.toString());
                            } else if (url?.toString() == 'about:blank') {
                              allowNavigation = true;
                            } else {
                              _logger.info('BLOCKED navigating within app, url: $url',
                                  stackTrace: StackTrace.current);
                            }

                            return Future<NavigationActionPolicy>.value(allowNavigation == true
                                ? NavigationActionPolicy.ALLOW
                                : NavigationActionPolicy.CANCEL);
                            },
                          );
                        } else {
                          return const SizedBox.shrink();
                        }
                      },
                    )
                  ],
              ));

@pichillilorenzo
Copy link
Owner

I cannot reproduce the issue using the latest plugin version.

Registrazione.schermo.2024-10-01.alle.10.32.59.mp4

Here is my test code:

import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:developer' as developer;

WebViewEnvironment? webViewEnvironment;

Future main() async {
  WidgetsFlutterBinding.ensureInitialized();

  PlatformInAppWebViewController.debugLoggingSettings.enabled = false;

  if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
    final availableVersion = await WebViewEnvironment.getAvailableVersion();
    assert(availableVersion != null,
        'Failed to find an installed WebView2 Runtime or non-stable Microsoft Edge installation.');

    webViewEnvironment = await WebViewEnvironment.create(
        settings:
            WebViewEnvironmentSettings(userDataFolder: 'YOUR_CUSTOM_PATH'));
  }

  if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
    await InAppWebViewController.setWebContentsDebuggingEnabled(kDebugMode);
  }

  runApp(const MaterialApp(home: MyApp()));
}

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

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey webViewKey = GlobalKey();

  InAppWebViewController? webViewController;
  InAppWebViewSettings settings = InAppWebViewSettings(
      isInspectable: true,
      allowFileAccessFromFileURLs: true,
      allowUniversalAccessFromFileURLs: true,
      mediaPlaybackRequiresUserGesture: false,
      applicationNameForUserAgent: "Test", // JUST FOR TESTING
      transparentBackground: true,
      useHybridComposition: true,
      forceDark: ForceDark.AUTO,
      allowsInlineMediaPlayback: true,
      disallowOverScroll: true,
      sharedCookiesEnabled: true,
      allowsBackForwardNavigationGestures: false);

  String url = "";
  double progress = 0;
  final urlController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: const Text("Official InAppWebView website")),
        body: SafeArea(
            child: Column(children: <Widget>[
          TextField(
            decoration: const InputDecoration(prefixIcon: Icon(Icons.search)),
            controller: urlController,
            keyboardType: TextInputType.url,
            onSubmitted: (value) {
              var url = WebUri(value);
              if (url.scheme.isEmpty) {
                url = WebUri("https://www.google.com/search?q=$value");
              }
              webViewController?.loadUrl(urlRequest: URLRequest(url: url));
            },
          ),
          Expanded(
            child: Stack(
              children: [
                InAppWebView(
                  key: webViewKey,
                  webViewEnvironment: webViewEnvironment,
                  initialUrlRequest: // JUST FOR TESTING
                      URLRequest(url: WebUri("https://inappwebview.dev/")),
                  initialSettings: settings,
                  onWebViewCreated: (controller) {
                    webViewController = controller;
                  },
                  shouldOverrideUrlLoading:
                      (controller, navigationAction) async {
                    developer.log(
                      'shouldOverrideUrlLoading: $navigationAction',
                      stackTrace: StackTrace.current,
                    );
                    bool allowNavigation = false;
                    Uri? url = navigationAction.request.url;

                    if (url?.scheme == 'blob') {
                      developer.log('download request.url $url',
                          stackTrace: StackTrace.current);
                    } else if (url?.scheme == 'https') {
                      if (url != null &&
                          (url.host == 'inappwebview.dev' || // JUST FOR TESTING
                              url.host == 'www.youtube.com' ||
                              url.host.endsWith('.plaid.com') ||
                              url.host.endsWith('.driftt.com'))) {
                        allowNavigation = true;
                      } else {
                        developer.log(
                            'BLOCKED navigating within app, url: $url',
                            stackTrace: StackTrace.current);
                        launchUrl(url!);
                      }
                    } else if (url?.scheme == 'tel') {
                      launchUrl(url!);
                    } else if (url?.toString() == 'about:blank') {
                      allowNavigation = true;
                    } else {
                      developer.log('BLOCKED navigating within app, url: $url',
                          stackTrace: StackTrace.current);
                    }

                    return allowNavigation == true
                        ? NavigationActionPolicy.ALLOW
                        : NavigationActionPolicy.CANCEL;
                  },
                  onProgressChanged: (controller, progress) {
                    setState(() {
                      this.progress = progress / 100;
                      urlController.text = url;
                    });
                  },
                  onLoadStop: (controller, url) async {
                    // JUST FOR TESTING
                    await Future.delayed(Duration(seconds: 1));
                    await controller.evaluateJavascript(
                        source:
                            '''window.open('https://google.com', '_blank');''');
                  },
                ),
                progress < 1.0
                    ? LinearProgressIndicator(value: progress)
                    : Container(),
              ],
            ),
          ),
          ButtonBar(
            alignment: MainAxisAlignment.center,
            children: <Widget>[
              ElevatedButton(
                child: const Icon(Icons.arrow_back),
                onPressed: () {
                  webViewController?.goBack();
                },
              ),
              ElevatedButton(
                child: const Icon(Icons.arrow_forward),
                onPressed: () {
                  webViewController?.goForward();
                },
              ),
              ElevatedButton(
                child: const Icon(Icons.refresh),
                onPressed: () {
                  webViewController?.reload();
                },
              ),
            ],
          ),
        ])));
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants