diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md new file mode 100644 index 000000000000..1a85bc8a53e5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -0,0 +1,3 @@ +## 2.0.13 + +* Extract WKWebView implementation from `webview_flutter`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/LICENSE b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md new file mode 100644 index 000000000000..2e3a87b7f310 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -0,0 +1,11 @@ +# webview\_flutter\_wkwebview + +The Apple WKWebView implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg new file mode 100644 index 000000000000..27e17104277b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 new file mode 100644 index 000000000000..a203d0cdf13e Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..33d5b340fa5a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1213 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_decision.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; +import 'package:webview_flutter_wkwebview_example/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. + const bool _skipDueToIssue86757 = false; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl('https://www.google.com/'); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }, skip: _skipDueToIssue86757); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'https://flutter.dev/', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: false, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
+ + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com/"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.google.com"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://www.google.com/'); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://flutter.dev/', + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('window.open("https://flutter.dev/", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter.dev/'); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: 'https://flutter.dev', + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller + .evaluateJavascript('window.open("https://www.google.com/")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion('https://www.google.com/')); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion('https://flutter.dev/')); + }, + skip: _skipDueToIssue86757, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return await controller.evaluateJavascript(js); + } + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 9.0 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..e8efba114687 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..399e9340e6f6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile new file mode 100644 index 000000000000..66509fcae284 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile @@ -0,0 +1,45 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'RunnerTests' do + inherit! :search_paths + + # Matches test_spec dependency. + pod 'OCMock', '3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..62428d041adf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F79266057800028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; + F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F71266057800028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, + 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, + F7151F75266057800028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + C6FFB52F5C2B8A41A7E39DE2 /* Pods */, + B6736FC417BDCCDA377E779D /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */, + F7151F74266057800028CB91 /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + B6736FC417BDCCDA377E779D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F75266057800028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F76266057800028CB91 /* FLTWebViewUITests.m */, + F7151F78266057800028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 68BDCAE523C3F7CB00D9C032 /* Sources */, + 68BDCAE623C3F7CB00D9C032 /* Frameworks */, + 68BDCAE723C3F7CB00D9C032 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = webview_flutter_exampleTests; + productReference = 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + F7151F73266057800028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F70266057800028CB91 /* Sources */, + F7151F71266057800028CB91 /* Frameworks */, + F7151F72266057800028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F7A266057800028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F74266057800028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 68BDCAE823C3F7CB00D9C032 = { + ProvisioningStyle = Automatic; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F73266057800028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */, + F7151F73266057800028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 68BDCAE723C3F7CB00D9C032 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F72266057800028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 68BDCAE523C3F7CB00D9C032 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F70266057800028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */; + }; + F7151F7A266057800028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F79266057800028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 68BDCAF023C3F7CB00D9C032 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 68BDCAF123C3F7CB00D9C032 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + F7151F7C266057800028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F7151F7D266057800028CB91 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 68BDCAF023C3F7CB00D9C032 /* Debug */, + 68BDCAF123C3F7CB00D9C032 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F7C266057800028CB91 /* Debug */, + F7151F7D266057800028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..d7453a8ce862 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..21a3cc14c74e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000000..d36b1fab2d9d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000000..28c6bf03016f Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000000..f091b6b0bca8 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000000..4cde12118dda Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000000..d0ef06e7edb8 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000000..dcdc2306c285 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000000..2ccbfd967d96 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000000..c8f9ed8f5cee Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000000..a6d6b8609df0 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000000..75b2d164a5a9 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000000..c4df70d39da7 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000000..6a84f41e14e2 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000000..d0e1f5853602 Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000000..0bedcf2fd467 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000000..9da19eacad3b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000000..89c2725b70f1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000000..f2e259c7c939 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000000..f3c28516fb38 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a810c5a172c0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + webview_flutter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m @@ -0,0 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m new file mode 100644 index 000000000000..eb6d1543ec07 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTWKNavigationDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; +@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; + +@end + +@implementation FLTWKNavigationDelegateTests + +- (void)setUp { + self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); + self.navigationDelegate = + [[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel]; +} + +- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { + if (@available(iOS 9.0, *)) { + // `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above. + WKWebView *webview = OCMClassMock(WKWebView.class); + [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; + OCMVerify([self.mockMethodChannel + invokeMethod:@"onWebResourceError" + arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { + XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated"); + return true; + }]]); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m new file mode 100644 index 000000000000..631c4a105063 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FLTWebViewTests : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; + +@end + +@implementation FLTWebViewTests + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); +} + +- (void)testCanInitFLTWebViewController { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(controller); +} + +- (void)testCanInitFLTWebViewFactory { + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(factory); +} + +- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { + if (@available(iOS 11, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, + UIScrollViewContentInsetAdjustmentNever); + } +} + +- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { + if (@available(iOS 13, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); + } +} + +- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { + FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; + webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); + XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 200); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); + + if (@available(iOS 11, *)) { + // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. + UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); + UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); + OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 100); + XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m new file mode 100644 index 000000000000..d193be745972 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import XCTest; +@import os.log; + +@interface FLTWebViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation FLTWebViewUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testUserAgent { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* userAgent = app.buttons[@"Show user agent"]; + if (![userAgent waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Show user agent"); + } + NSPredicate* userAgentPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; + XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCTAssertFalse(userAgentPopUp.exists); + [userAgent tap]; + if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find user agent pop up"); + } +} + +- (void)testCache { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* clearCache = app.buttons[@"Clear cache"]; + if (![clearCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Clear cache"); + } + [clearCache tap]; + + [menu tap]; + + XCUIElement* listCache = app.buttons[@"List cache"]; + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find empty cache pop up"); + } + + [menu tap]; + XCUIElement* addCache = app.buttons[@"Add to cache"]; + if (![addCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Add to cache"); + } + [addCache tap]; + [menu tap]; + + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* cachePopup = + app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" + @"localStorage\":\"dummy_entry\"}}"]; + if (![cachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find cache pop up"); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart new file mode 100644 index 000000000000..a953e062ded5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; +import 'web_view.dart'; + +void main() { + runApp(MaterialApp(home: _WebViewExample())); +} + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

+The navigation delegate is set to block navigation to the youtube website. +

+ + + +'''; + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + _NavigationControls(_controller.future), + _SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (context) { + return WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +Set _createJavascriptChannels(BuildContext context) { + return { + JavascriptChannel( + name: 'Snackbar', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message.message))); + }), + }; +} + +enum _MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class _SampleMenu extends StatelessWidget { + _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case _MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case _MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case _MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case _MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case _MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case _MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem<_MenuOptions>( + value: _MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Snackbar JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Snackbar.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await WebView.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class _NavigationControls extends StatelessWidget { + const _NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + if (controller == null) return Container(); + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart new file mode 100644 index 000000000000..c1ff8dc5a690 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart new file mode 100644 index 000000000000..ddb8e9b0f14f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -0,0 +1,592 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + /// The WebView platform that's used by this WebView. + static final WebViewPlatform platform = CupertinoWebView(); + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller._updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController._( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + userAgent: widget.userAgent, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels( + _javascriptChannelRegistry.channels.values.toSet()); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); + } + + Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; + } + +// Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._webView); + + final WebView _webView; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_webView.onPageStarted != null) { + _webView.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_webView.onPageFinished != null) { + _webView.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_webView.onProgress != null) { + _webView.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_webView.onWebResourceError != null) { + _webView.onWebResourceError!(error); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml new file mode 100644 index 000000000000..229da5e337a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_wkwebview_example +description: Demonstrates how to use the webview_flutter_wkwebview plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter_wkwebview: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep b/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h new file mode 100644 index 000000000000..8fe331875250 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTCookieManager : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m new file mode 100644 index 000000000000..f4783ffb4123 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTCookieManager.h" + +@implementation FLTCookieManager { +} + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FLTCookieManager *instance = [[FLTCookieManager alloc] init]; + + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/cookie_manager" + binaryMessenger:[registrar messenger]]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"clearCookies"]) { + [self clearCookies:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)clearCookies:(FlutterResult)result { + if (@available(iOS 9.0, *)) { + NSSet *websiteDataTypes = [NSSet setWithObject:WKWebsiteDataTypeCookies]; + WKWebsiteDataStore *dataStore = [WKWebsiteDataStore defaultDataStore]; + + void (^deleteAndNotify)(NSArray *) = + ^(NSArray *cookies) { + BOOL hasCookies = cookies.count > 0; + [dataStore removeDataOfTypes:websiteDataTypes + forDataRecords:cookies + completionHandler:^{ + result(@(hasCookies)); + }]; + }; + + [dataStore fetchDataRecordsOfTypes:websiteDataTypes completionHandler:deleteAndNotify]; + } else { + // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. + NSLog(@"Clearing cookies is not supported for Flutter WebViews prior to iOS 9."); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h new file mode 100644 index 000000000000..31edadc8cc05 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h @@ -0,0 +1,21 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKNavigationDelegate : NSObject + +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel; + +/** + * Whether to delegate navigation decisions over the method channel. + */ +@property(nonatomic, assign) BOOL hasDartNavigationDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m new file mode 100644 index 000000000000..8b7ee7d0cfb7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m @@ -0,0 +1,116 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKNavigationDelegate.h" + +@implementation FLTWKNavigationDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithChannel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + } + return self; +} + +#pragma mark - WKNavigationDelegate conformance + +- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation { + [_methodChannel invokeMethod:@"onPageStarted" arguments:@{@"url" : webView.URL.absoluteString}]; +} + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + if (!self.hasDartNavigationDelegate) { + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + NSDictionary *arguments = @{ + @"url" : navigationAction.request.URL.absoluteString, + @"isForMainFrame" : @(navigationAction.targetFrame.isMainFrame) + }; + [_methodChannel invokeMethod:@"navigationRequest" + arguments:arguments + result:^(id _Nullable result) { + if ([result isKindOfClass:[FlutterError class]]) { + NSLog(@"navigationRequest has unexpectedly completed with an error, " + @"allowing navigation."); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + if (result == FlutterMethodNotImplemented) { + NSLog(@"navigationRequest was unexepectedly not implemented: %@, " + @"allowing navigation.", + result); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + if (![result isKindOfClass:[NSNumber class]]) { + NSLog(@"navigationRequest unexpectedly returned a non boolean value: " + @"%@, allowing navigation.", + result); + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + NSNumber *typedResult = result; + decisionHandler([typedResult boolValue] ? WKNavigationActionPolicyAllow + : WKNavigationActionPolicyCancel); + }]; +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + [_methodChannel invokeMethod:@"onPageFinished" arguments:@{@"url" : webView.URL.absoluteString}]; +} + ++ (id)errorCodeToString:(NSUInteger)code { + switch (code) { + case WKErrorUnknown: + return @"unknown"; + case WKErrorWebContentProcessTerminated: + return @"webContentProcessTerminated"; + case WKErrorWebViewInvalidated: + return @"webViewInvalidated"; + case WKErrorJavaScriptExceptionOccurred: + return @"javaScriptExceptionOccurred"; + case WKErrorJavaScriptResultTypeIsUnsupported: + return @"javaScriptResultTypeIsUnsupported"; + } + + return [NSNull null]; +} + +- (void)onWebResourceError:(NSError *)error { + [_methodChannel invokeMethod:@"onWebResourceError" + arguments:@{ + @"errorCode" : @(error.code), + @"domain" : error.domain, + @"description" : error.description, + @"errorType" : [FLTWKNavigationDelegate errorCodeToString:error.code], + }]; +} + +- (void)webView:(WKWebView *)webView + didFailNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self onWebResourceError:error]; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + [self onWebResourceError:error]; +} + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { + NSError *contentProcessTerminatedError = + [[NSError alloc] initWithDomain:WKErrorDomain + code:WKErrorWebContentProcessTerminated + userInfo:nil]; + [self onWebResourceError:contentProcessTerminatedError]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h new file mode 100644 index 000000000000..96af4ef6c578 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKProgressionDelegate : NSObject + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; + +- (void)stopObservingProgress:(WKWebView *)webView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m new file mode 100644 index 000000000000..8e7af4649aa0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKProgressionDelegate.h" + +NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; + +@implementation FLTWKProgressionDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:FLTWKEstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + } + return self; +} + +- (void)stopObservingProgress:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { + NSNumber *newValue = + change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 + int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 + [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h new file mode 100644 index 000000000000..2a80c7d886f2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +@interface FLTWebViewFlutterPlugin : NSObject +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m new file mode 100644 index 000000000000..9f01416acc6a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWebViewFlutterPlugin.h" +#import "FLTCookieManager.h" +#import "FlutterWebView.h" + +@implementation FLTWebViewFlutterPlugin + ++ (void)registerWithRegistrar:(NSObject*)registrar { + FLTWebViewFactory* webviewFactory = + [[FLTWebViewFactory alloc] initWithMessenger:registrar.messenger]; + [registrar registerViewFactory:webviewFactory withId:@"plugins.flutter.io/webview"]; + [FLTCookieManager registerWithRegistrar:registrar]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h new file mode 100644 index 000000000000..6e795f7d1528 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -0,0 +1,32 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWebViewController : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; + +- (UIView*)view; +@end + +@interface FLTWebViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject*)messenger; +@end + +/** + * The WkWebView used for the plugin. + * + * This class overrides some methods in `WKWebView` to serve the needs for the plugin. + */ +@interface FLTWKWebView : WKWebView +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m new file mode 100644 index 000000000000..c6d926d3cfc2 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -0,0 +1,491 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FlutterWebView.h" +#import "FLTWKNavigationDelegate.h" +#import "FLTWKProgressionDelegate.h" +#import "JavaScriptChannelHandler.h" + +@implementation FLTWebViewFactory { + NSObject* _messenger; +} + +- (instancetype)initWithMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + FLTWebViewController* webviewController = [[FLTWebViewController alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; + return webviewController; +} + +@end + +@implementation FLTWKWebView + +- (void)setFrame:(CGRect)frame { + [super setFrame:frame]; + self.scrollView.contentInset = UIEdgeInsetsZero; + // We don't want the contentInsets to be adjusted by iOS, flutter should always take control of + // webview's contentInsets. + // self.scrollView.contentInset = UIEdgeInsetsZero; + if (@available(iOS 11, *)) { + // Above iOS 11, adjust contentInset to compensate the adjustedContentInset so the sum will + // always be 0. + if (UIEdgeInsetsEqualToEdgeInsets(self.scrollView.adjustedContentInset, UIEdgeInsetsZero)) { + return; + } + UIEdgeInsets insetToAdjust = self.scrollView.adjustedContentInset; + self.scrollView.contentInset = UIEdgeInsetsMake(-insetToAdjust.top, -insetToAdjust.left, + -insetToAdjust.bottom, -insetToAdjust.right); + } +} + +@end + +@implementation FLTWebViewController { + FLTWKWebView* _webView; + int64_t _viewId; + FlutterMethodChannel* _channel; + NSString* _currentUrl; + // The set of registered JavaScript channel names. + NSMutableSet* _javaScriptChannelNames; + FLTWKNavigationDelegate* _navigationDelegate; + FLTWKProgressionDelegate* _progressionDelegate; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger { + if (self = [super init]) { + _viewId = viewId; + + NSString* channelName = [NSString stringWithFormat:@"plugins.flutter.io/webview_%lld", viewId]; + _channel = [FlutterMethodChannel methodChannelWithName:channelName binaryMessenger:messenger]; + _javaScriptChannelNames = [[NSMutableSet alloc] init]; + + WKUserContentController* userContentController = [[WKUserContentController alloc] init]; + if ([args[@"javascriptChannelNames"] isKindOfClass:[NSArray class]]) { + NSArray* javaScriptChannelNames = args[@"javascriptChannelNames"]; + [_javaScriptChannelNames addObjectsFromArray:javaScriptChannelNames]; + [self registerJavaScriptChannels:_javaScriptChannelNames controller:userContentController]; + } + + NSDictionary* settings = args[@"settings"]; + + WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + [self applyConfigurationSettings:settings toConfiguration:configuration]; + configuration.userContentController = userContentController; + [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] + inConfiguration:configuration]; + + _webView = [[FLTWKWebView alloc] initWithFrame:frame configuration:configuration]; + _navigationDelegate = [[FLTWKNavigationDelegate alloc] initWithChannel:_channel]; + _webView.UIDelegate = self; + _webView.navigationDelegate = _navigationDelegate; + __weak __typeof__(self) weakSelf = self; + [_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [weakSelf onMethodCall:call result:result]; + }]; + + if (@available(iOS 11.0, *)) { + _webView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; + if (@available(iOS 13.0, *)) { + _webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = NO; + } + } + + [self applySettings:settings]; + // TODO(amirh): return an error if apply settings failed once it's possible to do so. + // https://github.com/flutter/flutter/issues/36228 + + NSString* initialUrl = args[@"initialUrl"]; + if ([initialUrl isKindOfClass:[NSString class]]) { + [self loadUrl:initialUrl]; + } + } + return self; +} + +- (void)dealloc { + if (_progressionDelegate != nil) { + [_progressionDelegate stopObservingProgress:_webView]; + } +} + +- (UIView*)view { + return _webView; +} + +- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"updateSettings"]) { + [self onUpdateSettings:call result:result]; + } else if ([[call method] isEqualToString:@"loadUrl"]) { + [self onLoadUrl:call result:result]; + } else if ([[call method] isEqualToString:@"canGoBack"]) { + [self onCanGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"canGoForward"]) { + [self onCanGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"goBack"]) { + [self onGoBack:call result:result]; + } else if ([[call method] isEqualToString:@"goForward"]) { + [self onGoForward:call result:result]; + } else if ([[call method] isEqualToString:@"reload"]) { + [self onReload:call result:result]; + } else if ([[call method] isEqualToString:@"currentUrl"]) { + [self onCurrentUrl:call result:result]; + } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { + [self onEvaluateJavaScript:call result:result]; + } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { + [self onAddJavaScriptChannels:call result:result]; + } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { + [self onRemoveJavaScriptChannels:call result:result]; + } else if ([[call method] isEqualToString:@"clearCache"]) { + [self clearCache:result]; + } else if ([[call method] isEqualToString:@"getTitle"]) { + [self onGetTitle:result]; + } else if ([[call method] isEqualToString:@"scrollTo"]) { + [self onScrollTo:call result:result]; + } else if ([[call method] isEqualToString:@"scrollBy"]) { + [self onScrollBy:call result:result]; + } else if ([[call method] isEqualToString:@"getScrollX"]) { + [self getScrollX:call result:result]; + } else if ([[call method] isEqualToString:@"getScrollY"]) { + [self getScrollY:call result:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)onUpdateSettings:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* error = [self applySettings:[call arguments]]; + if (error == nil) { + result(nil); + return; + } + result([FlutterError errorWithCode:@"updateSettings_failed" message:error details:nil]); +} + +- (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { + if (![self loadRequest:[call arguments]]) { + result([FlutterError + errorWithCode:@"loadUrl_failed" + message:@"Failed parsing the URL" + details:[NSString stringWithFormat:@"Request was: '%@'", [call arguments]]]); + } else { + result(nil); + } +} + +- (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoBack = [_webView canGoBack]; + result(@(canGoBack)); +} + +- (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + BOOL canGoForward = [_webView canGoForward]; + result(@(canGoForward)); +} + +- (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goBack]; + result(nil); +} + +- (void)onGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView goForward]; + result(nil); +} + +- (void)onReload:(FlutterMethodCall*)call result:(FlutterResult)result { + [_webView reload]; + result(nil); +} + +- (void)onCurrentUrl:(FlutterMethodCall*)call result:(FlutterResult)result { + _currentUrl = [[_webView URL] absoluteString]; + result(_currentUrl); +} + +- (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)result { + NSString* jsString = [call arguments]; + if (!jsString) { + result([FlutterError errorWithCode:@"evaluateJavaScript_failed" + message:@"JavaScript String cannot be null" + details:nil]); + return; + } + [_webView evaluateJavaScript:jsString + completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { + if (error) { + result([FlutterError + errorWithCode:@"evaluateJavaScript_failed" + message:@"Failed evaluating JavaScript" + details:[NSString stringWithFormat:@"JavaScript string was: '%@'\n%@", + jsString, error]]); + } else { + result([NSString stringWithFormat:@"%@", evaluateResult]); + } + }]; +} + +- (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { + NSArray* channelNames = [call arguments]; + NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; + [_javaScriptChannelNames addObjectsFromArray:channelNames]; + [self registerJavaScriptChannels:channelNamesSet + controller:_webView.configuration.userContentController]; + result(nil); +} + +- (void)onRemoveJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { + // WkWebView does not support removing a single user script, so instead we remove all + // user scripts, all message handlers. And re-register channels that shouldn't be removed. + [_webView.configuration.userContentController removeAllUserScripts]; + for (NSString* channelName in _javaScriptChannelNames) { + [_webView.configuration.userContentController removeScriptMessageHandlerForName:channelName]; + } + + NSArray* channelNamesToRemove = [call arguments]; + for (NSString* channelName in channelNamesToRemove) { + [_javaScriptChannelNames removeObject:channelName]; + } + + [self registerJavaScriptChannels:_javaScriptChannelNames + controller:_webView.configuration.userContentController]; + result(nil); +} + +- (void)clearCache:(FlutterResult)result { + if (@available(iOS 9.0, *)) { + NSSet* cacheDataTypes = [WKWebsiteDataStore allWebsiteDataTypes]; + WKWebsiteDataStore* dataStore = [WKWebsiteDataStore defaultDataStore]; + NSDate* dateFrom = [NSDate dateWithTimeIntervalSince1970:0]; + [dataStore removeDataOfTypes:cacheDataTypes + modifiedSince:dateFrom + completionHandler:^{ + result(nil); + }]; + } else { + // support for iOS8 tracked in https://github.com/flutter/flutter/issues/27624. + NSLog(@"Clearing cache is not supported for Flutter WebViews prior to iOS 9."); + } +} + +- (void)onGetTitle:(FlutterResult)result { + NSString* title = _webView.title; + result(title); +} + +- (void)onScrollTo:(FlutterMethodCall*)call result:(FlutterResult)result { + NSDictionary* arguments = [call arguments]; + int x = [arguments[@"x"] intValue]; + int y = [arguments[@"y"] intValue]; + + _webView.scrollView.contentOffset = CGPointMake(x, y); + result(nil); +} + +- (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { + CGPoint contentOffset = _webView.scrollView.contentOffset; + + NSDictionary* arguments = [call arguments]; + int x = [arguments[@"x"] intValue] + contentOffset.x; + int y = [arguments[@"y"] intValue] + contentOffset.y; + + _webView.scrollView.contentOffset = CGPointMake(x, y); + result(nil); +} + +- (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { + int offsetX = _webView.scrollView.contentOffset.x; + result(@(offsetX)); +} + +- (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { + int offsetY = _webView.scrollView.contentOffset.y; + result(@(offsetY)); +} + +// Returns nil when successful, or an error message when one or more keys are unknown. +- (NSString*)applySettings:(NSDictionary*)settings { + NSMutableArray* unknownKeys = [[NSMutableArray alloc] init]; + for (NSString* key in settings) { + if ([key isEqualToString:@"jsMode"]) { + NSNumber* mode = settings[key]; + [self updateJsMode:mode]; + } else if ([key isEqualToString:@"hasNavigationDelegate"]) { + NSNumber* hasDartNavigationDelegate = settings[key]; + _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"hasProgressTracking"]) { + NSNumber* hasProgressTrackingValue = settings[key]; + bool hasProgressTracking = [hasProgressTrackingValue boolValue]; + if (hasProgressTracking) { + _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView + channel:_channel]; + } + } else if ([key isEqualToString:@"debuggingEnabled"]) { + // no-op debugging is always enabled on iOS. + } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { + NSNumber* allowsBackForwardNavigationGestures = settings[key]; + _webView.allowsBackForwardNavigationGestures = + [allowsBackForwardNavigationGestures boolValue]; + } else if ([key isEqualToString:@"userAgent"]) { + NSString* userAgent = settings[key]; + [self updateUserAgent:[userAgent isEqual:[NSNull null]] ? nil : userAgent]; + } else { + [unknownKeys addObject:key]; + } + } + if ([unknownKeys count] == 0) { + return nil; + } + return [NSString stringWithFormat:@"webview_flutter: unknown setting keys: {%@}", + [unknownKeys componentsJoinedByString:@", "]]; +} + +- (void)applyConfigurationSettings:(NSDictionary*)settings + toConfiguration:(WKWebViewConfiguration*)configuration { + NSAssert(configuration != _webView.configuration, + @"configuration needs to be updated before webView.configuration."); + for (NSString* key in settings) { + if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { + NSNumber* allowsInlineMediaPlayback = settings[key]; + configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; + } + } +} + +- (void)updateJsMode:(NSNumber*)mode { + WKPreferences* preferences = [[_webView configuration] preferences]; + switch ([mode integerValue]) { + case 0: // disabled + [preferences setJavaScriptEnabled:NO]; + break; + case 1: // unrestricted + [preferences setJavaScriptEnabled:YES]; + break; + default: + NSLog(@"webview_flutter: unknown JavaScript mode: %@", mode); + } +} + +- (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy + inConfiguration:(WKWebViewConfiguration*)configuration { + switch ([policy integerValue]) { + case 0: // require_user_action_for_all_media_types + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = true; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = true; +#pragma clang diagnostic pop + } + break; + case 1: // always_allow + if (@available(iOS 10.0, *)) { + configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = false; + } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + configuration.mediaPlaybackRequiresUserAction = false; +#pragma clang diagnostic pop + } + break; + default: + NSLog(@"webview_flutter: unknown auto media playback policy: %@", policy); + } +} + +- (bool)loadRequest:(NSDictionary*)request { + if (!request) { + return false; + } + + NSString* url = request[@"url"]; + if ([url isKindOfClass:[NSString class]]) { + id headers = request[@"headers"]; + if ([headers isKindOfClass:[NSDictionary class]]) { + return [self loadUrl:url withHeaders:headers]; + } else { + return [self loadUrl:url]; + } + } + + return false; +} + +- (bool)loadUrl:(NSString*)url { + return [self loadUrl:url withHeaders:[NSMutableDictionary dictionary]]; +} + +- (bool)loadUrl:(NSString*)url withHeaders:(NSDictionary*)headers { + NSURL* nsUrl = [NSURL URLWithString:url]; + if (!nsUrl) { + return false; + } + NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:nsUrl]; + [request setAllHTTPHeaderFields:headers]; + [_webView loadRequest:request]; + return true; +} + +- (void)registerJavaScriptChannels:(NSSet*)channelNames + controller:(WKUserContentController*)userContentController { + for (NSString* channelName in channelNames) { + FLTJavaScriptChannel* channel = + [[FLTJavaScriptChannel alloc] initWithMethodChannel:_channel + javaScriptChannelName:channelName]; + [userContentController addScriptMessageHandler:channel name:channelName]; + NSString* wrapperSource = [NSString + stringWithFormat:@"window.%@ = webkit.messageHandlers.%@;", channelName, channelName]; + WKUserScript* wrapperScript = + [[WKUserScript alloc] initWithSource:wrapperSource + injectionTime:WKUserScriptInjectionTimeAtDocumentStart + forMainFrameOnly:NO]; + [userContentController addUserScript:wrapperScript]; + } +} + +- (void)updateUserAgent:(NSString*)userAgent { + if (@available(iOS 9.0, *)) { + [_webView setCustomUserAgent:userAgent]; + } else { + NSLog(@"Updating UserAgent is not supported for Flutter WebViews prior to iOS 9."); + } +} + +#pragma mark WKUIDelegate + +- (WKWebView*)webView:(WKWebView*)webView + createWebViewWithConfiguration:(WKWebViewConfiguration*)configuration + forNavigationAction:(WKNavigationAction*)navigationAction + windowFeatures:(WKWindowFeatures*)windowFeatures { + if (!navigationAction.targetFrame.isMainFrame) { + [webView loadRequest:navigationAction.request]; + } + + return nil; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h new file mode 100644 index 000000000000..a0a5ec657295 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTJavaScriptChannel : NSObject + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel + javaScriptChannelName:(NSString*)javaScriptChannelName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m new file mode 100644 index 000000000000..ec9a363a4b2e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m @@ -0,0 +1,36 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "JavaScriptChannelHandler.h" + +@implementation FLTJavaScriptChannel { + FlutterMethodChannel* _methodChannel; + NSString* _javaScriptChannelName; +} + +- (instancetype)initWithMethodChannel:(FlutterMethodChannel*)methodChannel + javaScriptChannelName:(NSString*)javaScriptChannelName { + self = [super init]; + NSAssert(methodChannel != nil, @"methodChannel must not be null."); + NSAssert(javaScriptChannelName != nil, @"javaScriptChannelName must not be null."); + if (self) { + _methodChannel = methodChannel; + _javaScriptChannelName = javaScriptChannelName; + } + return self; +} + +- (void)userContentController:(WKUserContentController*)userContentController + didReceiveScriptMessage:(WKScriptMessage*)message { + NSAssert(_methodChannel != nil, @"Can't send a message to an unitialized JavaScript channel."); + NSAssert(_javaScriptChannelName != nil, + @"Can't send a message to an unitialized JavaScript channel."); + NSDictionary* arguments = @{ + @"channel" : _javaScriptChannelName, + @"message" : [NSString stringWithFormat:@"%@", message.body] + }; + [_methodChannel invokeMethod:@"javascriptChannelMessage" arguments:arguments]; +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec new file mode 100644 index 000000000000..37905f147489 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'webview_flutter_wkwebview' + s.version = '0.0.1' + s.summary = 'A WebView Plugin for Flutter.' + s.description = <<-DESC +A Flutter plugin that provides a WebView widget. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview' } + s.documentation_url = 'https://pub.dev/packages/webview_flutter' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart new file mode 100644 index 000000000000..05b79d0a72e4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an iOS webview. +/// +/// This is used as the default implementation for [WebView.platform] on iOS. It uses +/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class CupertinoWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart new file mode 100644 index 000000000000..bbec415dccd0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/webview_cupertino.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml new file mode 100644 index 000000000000..c6f6d6f94f07 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -0,0 +1,29 @@ +name: webview_flutter_wkwebview +description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.0.13 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + ios: + pluginClass: FLTWebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + webview_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 \ No newline at end of file