diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj index 62428d041adf..0759b31a2f25 100644 --- a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -11,13 +11,13 @@ 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 */; }; + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */; }; 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 */; }; + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0ABA59F25635F077C9EA161 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,11 +52,13 @@ /* 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 = ""; }; + 11DF059E983DF25F078B44CC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.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 = ""; }; + 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 4D2B3F45D8E6CA81EA52591E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; 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 = ""; }; @@ -64,7 +66,6 @@ 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; }; @@ -73,9 +74,8 @@ 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 = ""; }; + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = ""; }; @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + 63D2F2FB307F1F037702C198 /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + E6159E2B6496F35B1D4F4096 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +108,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C0ABA59F25635F077C9EA161 /* libPods-Runner.a */, + BEC8CD326B252E47ABE6C037 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,8 +146,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + EA36D6F90B795550E32A139A /* Pods */, + 00D2395F7DDFEE571DF3C0B1 /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +185,16 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + EA36D6F90B795550E32A139A /* Pods */ = { isa = PBXGroup; children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, - E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + 4D2B3F45D8E6CA81EA52591E /* Pods-Runner.debug.xcconfig */, + 11DF059E983DF25F078B44CC /* Pods-Runner.release.xcconfig */, + 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */, + 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + EA0C9BB56C9A98B4F095051B /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 1B3EA6BF26F6D525A8503093 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -338,41 +339,41 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 1B3EA6BF26F6D525A8503093 /* [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 = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); 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"; + 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; }; - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Thin Binary"; 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; + 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"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -388,18 +389,22 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + EA0C9BB56C9A98B4F095051B /* [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-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -473,7 +478,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 3CEFE8F0E91B9792E4EE427B /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +492,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 5D19D984A61169BB95DB0FED /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md index c0837263509d..176028f2b11c 100644 --- a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + ## 2.1.0 * Add `zoomEnabled` functionality. diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle index ef1485a765d0..bb9fe9708eaa 100644 --- a/packages/webview_flutter/webview_flutter_android/android/build.gradle +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -58,4 +58,8 @@ android { } } } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index b2a453adcdac..ed14107220b8 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -245,7 +245,11 @@ public void onMethodCall(MethodCall methodCall, Result result) { currentUrl(result); break; case "evaluateJavascript": - evaluateJavaScript(methodCall, result); + case "runJavascriptReturningResult": + evaluateJavaScript(methodCall, result, true); + break; + case "runJavascript": + evaluateJavaScript(methodCall, result, false); break; case "addJavascriptChannels": addJavaScriptChannels(methodCall, result); @@ -326,7 +330,8 @@ private void updateSettings(MethodCall methodCall, Result result) { } @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { + private void evaluateJavaScript( + MethodCall methodCall, final Result result, final boolean returnValue) { String jsString = (String) methodCall.arguments; if (jsString == null) { throw new UnsupportedOperationException("JavaScript string cannot be null"); @@ -336,7 +341,11 @@ private void evaluateJavaScript(MethodCall methodCall, final Result result) { new android.webkit.ValueCallback() { @Override public void onReceiveValue(String value) { - result.success(value); + if (returnValue) { + result.success(value); + } else { + result.success(null); + } } }); } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java index fd79bccabbce..f26a0ea6b9cc 100644 --- a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -6,31 +6,46 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; import android.webkit.WebView; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; import java.util.HashMap; import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.mockito.MockedStatic; public class FlutterWebViewTest { private WebChromeClient mockWebChromeClient; private DownloadListener mockDownloadListener; private WebViewBuilder mockWebViewBuilder; private WebView mockWebView; + private MethodChannel.Result mockResult; + private Context mockContext; + private MethodChannel mockMethodChannel; @Before public void before() { + mockWebChromeClient = mock(WebChromeClient.class); mockWebViewBuilder = mock(WebViewBuilder.class); mockWebView = mock(WebView.class); mockDownloadListener = mock(DownloadListener.class); + mockResult = mock(MethodChannel.Result.class); + mockContext = mock(Context.class); + mockMethodChannel = mock(MethodChannel.class); when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) @@ -42,12 +57,11 @@ public void before() { .thenReturn(mockWebViewBuilder); when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) .thenReturn(mockWebViewBuilder); - when(mockWebViewBuilder.build()).thenReturn(mockWebView); } @Test - public void createWebView_should_create_webview_with_default_configuration() { + public void createWebView_shouldCreateWebViewWithDefaultConfiguration() { FlutterWebView.createWebView( mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); @@ -59,6 +73,97 @@ public void createWebView_should_create_webview_with_default_configuration() { verify(mockWebViewBuilder, times(1)).setZoomControlsEnabled(true); } + @Test(expected = UnsupportedOperationException.class) + public void evaluateJavaScript_shouldThrowForNullString() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + new MockedStatic.Verification() { + @Override + public void apply() throws Throwable { + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any()); + } + }) + .thenReturn(mockWebView); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall(new MethodCall("runJavascript", null), mockResult); + } + } + + @Test + public void evaluateJavaScript_shouldReturnValueOnSuccessForReturnValue() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + () -> + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any())) + .thenReturn(mockWebView); + doAnswer( + invocation -> { + android.webkit.ValueCallback callback = invocation.getArgument(1); + callback.onReceiveValue("Test JavaScript Result"); + return null; + }) + .when(mockWebView) + .evaluateJavascript(eq("Test JavaScript String"), any()); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall( + new MethodCall("runJavascriptReturningResult", "Test JavaScript String"), mockResult); + + // Verify + verify(mockResult, times(1)).success("Test JavaScript Result"); + } + } + + @Test + public void evaluateJavaScript_shouldReturnNilOnSuccessForNoReturnValue() { + try (MockedStatic mockedFlutterWebView = mockStatic(FlutterWebView.class)) { + // Setup + mockedFlutterWebView + .when( + () -> + FlutterWebView.createWebView( + (WebViewBuilder) any(), + (Map) any(), + (WebChromeClient) any(), + (DownloadListener) any())) + .thenReturn(mockWebView); + doAnswer( + invocation -> { + android.webkit.ValueCallback callback = invocation.getArgument(1); + callback.onReceiveValue("Test JavaScript Result"); + return null; + }) + .when(mockWebView) + .evaluateJavascript(eq("Test JavaScript String"), any()); + FlutterWebView flutterWebView = + new FlutterWebView(mockContext, mockMethodChannel, new HashMap(), null); + + // Run + flutterWebView.onMethodCall( + new MethodCall("runJavascript", "Test JavaScript String"), mockResult); + + // Verify + verify(mockResult, times(1)).success(isNull()); + } + } + private Map createParameterMap(boolean usesHybridComposition) { Map params = new HashMap<>(); params.put("usesHybridComposition", usesHybridComposition); diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart index c57d2bd55580..8e3dcb458ee8 100644 --- a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -76,6 +76,27 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = @@ -114,12 +135,12 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }, skip: _skipDueToIssue86757); // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -159,11 +180,7 @@ void main() { 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;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }, skip: _skipDueToIssue86757); @@ -410,7 +427,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -439,7 +457,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -470,7 +488,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -498,7 +517,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -548,7 +567,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); }); @@ -614,7 +633,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -648,7 +668,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -684,7 +704,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -717,7 +738,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -982,15 +1003,16 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - final String viewportRectJSON = await _evaluateJavascript( + final String viewportRectJSON = await _runJavaScriptReturningResult( controller, 'JSON.stringify(viewport.getBoundingClientRect())'); final Map viewportRectRelativeToViewport = jsonDecode(viewportRectJSON); // Check that the input is originally outside of the viewport. - final String initialInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String initialInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map initialInputClientRectRelativeToViewport = jsonDecode(initialInputClientRectJSON); @@ -999,12 +1021,13 @@ void main() { viewportRectRelativeToViewport['bottom'], isFalse); - await controller.evaluateJavascript('inputEl.focus()'); + await controller.runJavascript('inputEl.focus()'); // Check that focusing the input brought it into view. - final String lastInputClientRectJSON = await _evaluateJavascript( - controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final String lastInputClientRectJSON = + await _runJavaScriptReturningResult( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); final Map lastInputClientRectRelativeToViewport = jsonDecode(lastInputClientRectJSON); @@ -1060,7 +1083,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1186,7 +1209,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('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 @@ -1226,7 +1249,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1281,7 +1304,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1317,7 +1340,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1331,7 +1354,7 @@ void main() { ); testWidgets( - 'javascript does not run in parent window', + 'JavaScript does not run in parent window', (WidgetTester tester) async { final String iframe = ''' @@ -1388,9 +1411,10 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoadCompleter.future; - expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect(controller.runJavascriptReturningResult('iframeLoaded'), + completion('true')); expect( - controller.evaluateJavascript( + controller.runJavascriptReturningResult( 'document.querySelector("p") && document.querySelector("p").textContent'), completion('null'), ); @@ -1409,10 +1433,10 @@ String _webviewBool(bool value) { /// 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;'); + return _runJavaScriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavaScriptReturningResult( WebViewController controller, String js) async { - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart index 65f49716aaac..a22d165a6ff3 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -195,14 +195,14 @@ class _SampleMenu extends StatelessWidget { 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( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(SnackBar( content: Column( @@ -217,7 +217,7 @@ class _SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); // ignore: deprecated_member_use Scaffold.of(context).showSnackBar(const SnackBar( @@ -226,7 +226,7 @@ class _SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } @@ -340,5 +340,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. +/// 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_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart index 5e8a2798b8ca..b43507447462 100644 --- a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -116,7 +116,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -126,7 +126,7 @@ class WebView extends StatefulWidget { /// 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: + /// For example for the following [JavascriptChannel]: /// /// ```dart /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); @@ -189,7 +189,7 @@ class WebView extends StatefulWidget { /// 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 + /// 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; @@ -487,31 +487,46 @@ class WebViewController { _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: + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// - 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. + /// When running 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 runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. /// - /// 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. + /// Returns the evaluation result as a JSON formatted string. + /// The Future completes with an error if a JavaScript error occurred. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// 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) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // 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); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml index 577d37359af1..ac208a09ebc0 100644 --- a/packages/webview_flutter/webview_flutter_android/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -2,7 +2,7 @@ name: webview_flutter_android description: A Flutter plugin that provides a WebView widget on Android. repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 -version: 2.1.0 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0" diff --git a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md index b75519af476c..4db6dbfd2864 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.2.0 + +* Implemented new `runJavascript` and `runJavascriptReturningResult` methods in platform interface. + ## 2.1.0 * Add `zoomEnabled` functionality. 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 index 8ba17a2428c5..17e896c94888 100644 --- 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 @@ -73,6 +73,27 @@ void main() { expect(currentUrl, secondaryUrl); }, skip: _skipDueToIssue86757); + testWidgets('evaluateJavascript', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String result = await controller.evaluateJavascript('1 + 1'); + expect(result, equals('2')); + }); + testWidgets('loadUrl with headers', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); @@ -110,11 +131,11 @@ void main() { await pageLoads.stream.firstWhere((String url) => url == currentUrl); final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); + .runJavascriptReturningResult('document.documentElement.innerText'); expect(content.contains('flutter_test_header'), isTrue); }); - testWidgets('JavaScriptChannel', (WidgetTester tester) async { + testWidgets('JavascriptChannel', (WidgetTester tester) async { final Completer controllerCompleter = Completer(); final Completer pageStarted = Completer(); @@ -154,11 +175,7 @@ void main() { 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;'); + await controller.runJavascript('Echo.postMessage("hello");'); expect(messagesReceived, equals(['hello'])); }); @@ -404,7 +421,8 @@ void main() { WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -433,7 +451,7 @@ void main() { controller = await controllerCompleter.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -464,7 +482,8 @@ void main() { final WebViewController controller = await controllerCompleter.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageLoaded = Completer(); @@ -492,7 +511,7 @@ void main() { await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); @@ -542,7 +561,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(false)); }); @@ -593,7 +612,7 @@ void main() { await videoPlaying.future; String fullScreen = - await controller.evaluateJavascript('isFullScreen();'); + await controller.runJavascriptReturningResult('isFullScreen();'); expect(fullScreen, _webviewBool(true)); }); }); @@ -659,7 +678,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); controllerCompleter = Completer(); @@ -693,7 +713,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(true)); }); @@ -729,7 +749,8 @@ void main() { await pageStarted.future; await pageLoaded.future; - String isPaused = await controller.evaluateJavascript('isPaused();'); + String isPaused = + await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); pageStarted = Completer(); @@ -762,7 +783,7 @@ void main() { await pageStarted.future; await pageLoaded.future; - isPaused = await controller.evaluateJavascript('isPaused();'); + isPaused = await controller.runJavascriptReturningResult('isPaused();'); expect(isPaused, _webviewBool(false)); }); }); @@ -919,7 +940,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for the next page load. final String? currentUrl = await controller.currentUrl(); @@ -1050,7 +1071,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); + .runJavascript('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 @@ -1090,7 +1111,7 @@ void main() { await pageLoads.stream.first; // Wait for initial page load. final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + await controller.runJavascript('location.href = "$secondaryUrl"'); await pageLoads.stream.first; // Wait for second page to load. final String? currentUrl = await controller.currentUrl(); @@ -1145,7 +1166,7 @@ void main() { ), ); final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await controller.runJavascript('window.open("$primaryUrl", "_blank")'); await pageLoaded.future; final String? currentUrl = await controller.currentUrl(); expect(currentUrl, primaryUrl); @@ -1179,7 +1200,7 @@ void main() { await pageLoaded.future; pageLoaded = Completer(); - await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await controller.runJavascript('window.open("$secondaryUrl")'); await pageLoaded.future; pageLoaded = Completer(); expect(controller.currentUrl(), completion(secondaryUrl)); @@ -1204,13 +1225,13 @@ String _webviewBool(bool value) { /// 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;'); + return _runJavascriptReturningResult(controller, 'navigator.userAgent;'); } -Future _evaluateJavascript( +Future _runJavascriptReturningResult( WebViewController controller, String js) async { if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript(js); + return await controller.runJavascriptReturningResult(js); } - return jsonDecode(await controller.evaluateJavascript(js)); + return jsonDecode(await controller.runJavascriptReturningResult(js)); } 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 index 62428d041adf..ba0deb4781d4 100644 --- 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 @@ -16,8 +16,8 @@ 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 */; }; + AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */; }; + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 572FFC2B2BA326B420B22679 /* libPods-Runner.a */; }; F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; /* End PBXBuildFile section */ @@ -52,11 +52,12 @@ /* 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 = ""; }; + 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; 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 = ""; }; @@ -64,7 +65,6 @@ 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; }; @@ -73,12 +73,12 @@ 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 = ""; }; + B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "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 = ""; }; + F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -86,7 +86,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + AE8C124DC8CA68E4D9B30EAB /* libPods-RunnerTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -94,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + DAF0E91266956134538CC667 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -108,6 +108,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 52FBC2B567345431F81A0A0F /* Frameworks */ = { + isa = PBXGroup; + children = ( + 572FFC2B2BA326B420B22679 /* libPods-Runner.a */, + 528CB85D53C983D2C5DAFDC5 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,8 +146,8 @@ 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, F7151F75266057800028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, + B8AEEA11D6ECBD09750349AE /* Pods */, + 52FBC2B567345431F81A0A0F /* Frameworks */, ); sourceTree = ""; }; @@ -176,24 +185,16 @@ name = "Supporting Files"; sourceTree = ""; }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { + B8AEEA11D6ECBD09750349AE /* Pods */ = { 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 */, + F7A1921261392D1CBDAEC2E8 /* Pods-Runner.debug.xcconfig */, + B89AA31A64040E4A2F1E0CAF /* Pods-Runner.release.xcconfig */, + C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */, + 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; F7151F75266057800028CB91 /* RunnerUITests */ = { @@ -212,7 +213,7 @@ isa = PBXNativeTarget; buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */, 68BDCAE523C3F7CB00D9C032 /* Sources */, 68BDCAE623C3F7CB00D9C032 /* Frameworks */, 68BDCAE723C3F7CB00D9C032 /* Resources */, @@ -231,7 +232,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, + 6F536C27DD48B395A30EBB65 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -338,21 +339,7 @@ /* 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 */ = { + 0067CEC0658A36CBFF8074E7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -374,30 +361,34 @@ 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 */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "Run Script"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + 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"; }; - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */ = { + 6F536C27DD48B395A30EBB65 /* [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-Runner-checkManifestLockResult.txt", ); @@ -406,6 +397,20 @@ 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"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -473,7 +478,7 @@ /* Begin XCBuildConfiguration section */ 68BDCAF023C3F7CB00D9C032 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = C370F140C3A19241FD8C5E64 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -487,7 +492,7 @@ }; 68BDCAF123C3F7CB00D9C032 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 5C776D27D0DDA247ED5EA72B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; 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 index 631c4a105063..9d127c2c4aaa 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -88,4 +88,217 @@ - (void)testContentInsetsSumAlwaysZeroAfterSetFrame { } } +- (void)testRunJavascriptFailsForNullString { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:nil] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptRunsStringWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(@"RESULT", nil); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturnsErrorResultForWKError { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + NSError *testError = + [NSError errorWithDomain:@"" + // Any error code but WKErrorJavascriptResultTypeIsUnsupported + code:WKErrorJavaScriptResultTypeIsUnsupported + 1 + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturnsSuccessForWKErrorJavascriptResultTypeIsUnsupported { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return nil result over the method channel."]; + NSError *testError = [NSError errorWithDomain:@"" + code:WKErrorJavaScriptResultTypeIsUnsupported + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascript" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultFailsForNullString { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:nil] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultRunsStringWithSuccessResult { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return successful result over the method channel."]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(@"RESULT", nil); + }]; + controller.webView = mockView; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([@"RESULT" isEqualToString:result]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + +- (void)testRunJavascriptReturningResultReturnsErrorResultForWKError { + // Setup + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTestExpectation *resultExpectation = + [self expectationWithDescription:@"Should return error result over the method channel."]; + NSError *testError = [NSError errorWithDomain:@"" + code:5 + userInfo:@{NSLocalizedDescriptionKey : @"Test Error"}]; + FLTWKWebView *mockView = OCMClassMock(FLTWKWebView.class); + [OCMStub([mockView evaluateJavaScript:[OCMArg any] + completionHandler:[OCMArg any]]) andDo:^(NSInvocation *invocation) { + // __unsafe_unretained: https://github.com/erikdoe/ocmock/issues/384#issuecomment-589376668 + __unsafe_unretained void (^evalResultHandler)(id, NSError *); + [invocation getArgument:&evalResultHandler atIndex:3]; + evalResultHandler(nil, testError); + }]; + controller.webView = mockView; + + // Run + [controller + onMethodCall:[FlutterMethodCall methodCallWithMethodName:@"runJavascriptReturningResult" + arguments:@"Test JavaScript String"] + result:^(id _Nullable result) { + XCTAssertTrue([result class] == [FlutterError class]); + [resultExpectation fulfill]; + }]; + + // Verify + [self waitForExpectationsWithTimeout:30.0 handler:nil]; +} + @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart index 15b4cfc7c549..21240f63ec1a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -199,14 +199,14 @@ class _SampleMenu extends StatelessWidget { 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( + await controller.runJavascript( 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); } void _onListCookies( WebViewController controller, BuildContext context) async { final String cookies = - await controller.evaluateJavascript('document.cookie'); + await controller.runJavascriptReturningResult('document.cookie'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Column( mainAxisAlignment: MainAxisAlignment.end, @@ -220,7 +220,7 @@ class _SampleMenu extends StatelessWidget { } void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( + await controller.runJavascript( 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('Added a test entry to cache.'), @@ -228,7 +228,7 @@ class _SampleMenu extends StatelessWidget { } void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' + await controller.runJavascript('caches.keys()' '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' '.then((caches) => Snackbar.postMessage(caches))'); } @@ -340,5 +340,5 @@ class _NavigationControls extends StatelessWidget { } } -/// Callback type for handling messages sent from Javascript running in a web view. +/// 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/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart index d57499dcef50..403db1f08ac6 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -94,7 +94,7 @@ class WebView extends StatefulWidget { /// The initial URL to load. final String? initialUrl; - /// Whether Javascript execution is enabled. + /// Whether JavaScript execution is enabled. final JavascriptMode javascriptMode; /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. @@ -167,7 +167,7 @@ class WebView extends StatefulWidget { /// 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 + /// 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; @@ -416,31 +416,51 @@ class WebViewController { _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. + @visibleForTesting + // ignore: public_member_api_docs + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Runs the given JavaScript in the context of the current page. + /// If you are looking for the result, use [runJavascriptReturningResult] instead. + /// The Future completes with an error if a JavaScript error occurred. /// - /// On iOS depending on the value type the return value would be one of: + /// When running 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 runJavascript(String javaScriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'Javascript mode must be enabled/unrestricted when calling runJavascript.')); + } + return _webViewPlatformController.runJavascript(javaScriptString); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. /// + /// 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. + /// The Future completes with an error if a JavaScript error occurred, or if the + /// type the given expression evaluates to is unsupported. Unsupported values include + /// certain non primitive types, as well as `undefined` or `null` on iOS 14+. /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// 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) { + Future runJavascriptReturningResult(String javaScriptString) { if (_settings.javascriptMode == JavascriptMode.disabled) { return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + 'Javascript mode must be enabled/unrestricted when calling runJavascriptReturningResult.')); } - // 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); + return _webViewPlatformController + .runJavascriptReturningResult(javaScriptString); } /// Returns the title of the currently loaded page. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 6e795f7d1528..db52124d6a7c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h @@ -7,26 +7,30 @@ NS_ASSUME_NONNULL_BEGIN +/** + * The WkWebView used for the plugin. + * + * This class overrides some methods in `WKWebView` to serve the needs for the plugin. + */ +@interface FLTWKWebView : WKWebView +@end + @interface FLTWebViewController : NSObject +@property(nonatomic) FLTWKWebView* webView; + - (instancetype)initWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject*)messenger; - (UIView*)view; + +- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; @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 index 7fd78ee302d0..5e12f8acb2ea 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -151,6 +151,10 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [self onCurrentUrl:call result:result]; } else if ([[call method] isEqualToString:@"evaluateJavascript"]) { [self onEvaluateJavaScript:call result:result]; + } else if ([[call method] isEqualToString:@"runJavascript"]) { + [self onRunJavaScript:call result:result sendReturnValue:NO]; + } else if ([[call method] isEqualToString:@"runJavascriptReturningResult"]) { + [self onRunJavaScript:call result:result sendReturnValue:YES]; } else if ([[call method] isEqualToString:@"addJavascriptChannels"]) { [self onAddJavaScriptChannels:call result:result]; } else if ([[call method] isEqualToString:@"removeJavascriptChannels"]) { @@ -244,6 +248,41 @@ - (void)onEvaluateJavaScript:(FlutterMethodCall*)call result:(FlutterResult)resu }]; } +- (void)onRunJavaScript:(FlutterMethodCall*)call + result:(FlutterResult)result + sendReturnValue:(BOOL)sendReturnValue { + NSString* jsString = [call arguments]; + if (!jsString) { + result([FlutterError errorWithCode:@"runJavascript_failed" + message:@"JavaScript String cannot be null" + details:nil]); + return; + } + [_webView + evaluateJavaScript:jsString + completionHandler:^(_Nullable id evaluateResult, NSError* _Nullable error) { + if (error) { + // WebKit will throw an error (WKErrorJavaScriptResultTypeIsUnsupported) when the + // type of the evaluated value is unsupported. This also goes for + // `null` and `undefined` on iOS 14+, for example when running a void function. + // For ease of use this specific error is ignored when no return value is expected. + BOOL sendError = + sendReturnValue || error.code != WKErrorJavaScriptResultTypeIsUnsupported; + result(sendError + ? [FlutterError + errorWithCode:(sendReturnValue ? @"runJavascriptReturningResult_failed" + : @"runJavascript_failed") + message:@"Failed running JavaScript" + details:[NSString + stringWithFormat:@"JavaScript string was: '%@'\n%@", + jsString, error]] + : nil); + return; + } + result(sendReturnValue ? [NSString stringWithFormat:@"%@", evaluateResult] : nil); + }]; +} + - (void)onAddJavaScriptChannels:(FlutterMethodCall*)call result:(FlutterResult)result { NSArray* channelNames = [call arguments]; NSSet* channelNamesSet = [[NSSet alloc] initWithArray:channelNames]; 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 index 37905f147489..2dfb336df35a 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -18,6 +18,6 @@ Downloaded by pub (not CocoaPods). s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml index bfa4b8050e90..5176adb9749c 100644 --- a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -2,7 +2,7 @@ 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.1.0 +version: 2.2.0 environment: sdk: ">=2.14.0 <3.0.0"