@capgo/capacitor-inappbrowser 8.21.2 → 8.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@ Open web content inside your Capacitor app with a full-featured in-app browser:
5
5
  <a href="https://capgo.app/?ref=plugin_inappbrowser"><img src="https://capgo.app/readme-banner.svg?repo=Cap-go/capacitor-inappbrowser" alt="Capgo - Instant updates for Capacitor" /></a>
6
6
 
7
7
  <div align="center">
8
- <p><b>Capgo</b>: open-source live updates for Ionic and Capacitor apps. Ship OTA fixes and features instantly, without waiting for app store review.</p>
8
+ <p><b>Capgo</b>: push fixes to your Capacitor users in minutes, build signed iOS and Android apps without a Mac, and roll back in one click.</p>
9
9
  <h2><a href="https://capgo.app/register/?ref=plugin_inappbrowser">➡️ Get started for free</a></h2>
10
10
  <p>14-day unlimited free trial. No credit card required</p>
11
11
  <p><a href="https://capgo.app/consulting/?ref=plugin_inappbrowser">Missing a feature? We'll build the plugin for you 💪</a></p>
@@ -1714,6 +1714,7 @@ See the README section "openSecureWindow (OAuth)" for full setup examples.
1714
1714
  | **`backgroundColor`** | <code><a href="#backgroundcolor">BackgroundColor</a></code> | Background color of the browser | <code>BackgroundColor.BLACK</code> | 0.1.0 |
1715
1715
  | **`activeNativeNavigationForWebview`** | <code>boolean</code> | If true, enables native navigation gestures within the webview. - Android: Native back button navigates within webview history - iOS: Enables swipe left/right gestures for back/forward navigation | <code>false (Android), true (iOS - enabled by default)</code> | |
1716
1716
  | **`enableReloadGesture`** | <code>boolean</code> | Enable pull-to-refresh (overscroll from top) to reload the current page. - iOS: Uses UIRefreshControl on the WebView scroll view (reload commits on finger release) - Android: Uses SwipeRefreshLayout around the WebView On iOS, this requires overscroll bounce. If `disableOverscroll` is `true`, the reload gesture will not work. | <code>false</code> | 8.10.8 |
1717
+ | **`allowWebViewFullscreen`** | <code>boolean</code> | Allow pages to enter HTML5 fullscreen (e.g. `requestFullscreen()` or a video player's fullscreen button). When `false`, fullscreen requests are rejected and the toolbar stays visible, so the user can always close the browser. Android only. On iOS, element fullscreen is not enabled by the plugin, so this option has no effect. | <code>true</code> | 8.22.0 |
1717
1718
  | **`disableGoBackOnNativeApplication`** | <code>boolean</code> | Disable the possibility to go back on native application, useful to force user to stay on the webview, Android only | <code>false</code> | |
1718
1719
  | **`isPresentAfterPageLoad`** | <code>boolean</code> | Open url in a new window fullscreen isPresentAfterPageLoad: if true, the browser will be presented after the page is loaded, if false, the browser will be presented immediately. Promise timing: on Android, `openWebView()` resolves with the webview id when the webview is ready to be controlled (immediately for hidden/immediate presentation, after the first page load when `isPresentAfterPageLoad` is `true`). On iOS, the promise resolves with the id as soon as the native webview is created, even if presentation is deferred. | <code>false</code> | 0.1.0 |
1719
1720
  | **`preferredContentMode`** | <code><a href="#preferredcontentmode">PreferredContentMode</a></code> | Web content rendering mode for the managed WKWebView. Controls whether pages load with mobile or desktop layout on iPad. Matches Capacitor's `ios.preferredContentMode` behavior. Resolution order when opening a webview: 1. This per-open value 2. Plugin config `plugins.CapgoInAppBrowser.preferredContentMode` 3. Legacy plugin config `plugins.InAppBrowser.preferredContentMode` 4. Capacitor config `ios.preferredContentMode` When unset at all levels, the system default applies (device-recommended). **iOS only** — ignored on Android and Web. | | 8.18.0 |
@@ -1286,6 +1286,7 @@ public class CapgoInAppBrowserPlugin extends Plugin implements WebViewDialog.Per
1286
1286
 
1287
1287
  options.setActiveNativeNavigationForWebview(Boolean.TRUE.equals(call.getBoolean("activeNativeNavigationForWebview", false)));
1288
1288
  options.setEnableReloadGesture(Boolean.TRUE.equals(call.getBoolean("enableReloadGesture", false)));
1289
+ options.setAllowWebViewFullscreen(!Boolean.FALSE.equals(call.getBoolean("allowWebViewFullscreen", true)));
1289
1290
  options.setDisableGoBackOnNativeApplication(Boolean.TRUE.equals(call.getBoolean("disableGoBackOnNativeApplication", false)));
1290
1291
  options.setPresentAfterPageLoad(Boolean.TRUE.equals(call.getBoolean("isPresentAfterPageLoad", false)));
1291
1292
  options.setPluginCall(call);
@@ -0,0 +1,150 @@
1
+ package ee.forgr.capacitor_inappbrowser;
2
+
3
+ /**
4
+ * Decides whether a file chooser request should use the direct camera capture path and
5
+ * builds JavaScript used to detect {@code capture} on file inputs (including shadow DOM).
6
+ */
7
+ final class FileInputCaptureChooserSupport {
8
+
9
+ private FileInputCaptureChooserSupport() {}
10
+
11
+ static boolean usesCameraCapturePath(boolean imageOnlyAcceptTypes, boolean captureEnabled) {
12
+ return imageOnlyAcceptTypes && captureEnabled;
13
+ }
14
+
15
+ /**
16
+ * Document-level click hook installed at document start (and on navigation fallback).
17
+ */
18
+ static String createFileInputCaptureHookScript() {
19
+ return (
20
+ """
21
+ (function() {
22
+ if (window.__capgoFileInputCaptureHook) {
23
+ return;
24
+ }
25
+ window.__capgoFileInputCaptureHook = true;
26
+ window.__capgoLastFileCapture = null;
27
+ """ +
28
+ captureDetectionHelpersJs() +
29
+ """
30
+ var notifyCapture = function(value) {
31
+ var captureValue = value || 'environment';
32
+ window.__capgoLastFileCapture = captureValue;
33
+ try {
34
+ var bridge = window.AndroidInterface || window.mobileApp;
35
+ if (bridge && bridge.setFileInputCapture) {
36
+ bridge.setFileInputCapture(captureValue);
37
+ }
38
+ } catch (err) {}
39
+ };
40
+ var register = function(doc) {
41
+ if (!doc || doc.__capgoFileInputCaptureRegistered) {
42
+ return;
43
+ }
44
+ doc.__capgoFileInputCaptureRegistered = true;
45
+ doc.addEventListener('click', function(e) {
46
+ var captureValue = capgoFindCaptureFromClickEvent(e);
47
+ if (captureValue) {
48
+ notifyCapture(captureValue);
49
+ }
50
+ }, true);
51
+ };
52
+ register(document);
53
+ window.addEventListener('load', function(e) {
54
+ var el = e.target;
55
+ try {
56
+ if (el && el.tagName === 'IFRAME' && el.contentDocument) {
57
+ register(el.contentDocument);
58
+ }
59
+ } catch (err) {}
60
+ }, true);
61
+ })();
62
+ """
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Runs when the native chooser opens, after {@code consumeLastFileInputCaptureValue()}.
68
+ */
69
+ static String createChooserFallbackCaptureResolutionScript() {
70
+ return (
71
+ """
72
+ (function() {
73
+ try {
74
+ """ +
75
+ captureDetectionHelpersJs() +
76
+ """
77
+ var cached = window.__capgoLastFileCapture;
78
+ window.__capgoLastFileCapture = null;
79
+ if (cached) {
80
+ return cached;
81
+ }
82
+ var fromActive = capgoFindCaptureFromActiveElementChain();
83
+ if (fromActive) {
84
+ return fromActive;
85
+ }
86
+ var inputs = document.querySelectorAll('input[type="file"][capture]');
87
+ if (inputs && inputs.length === 1) {
88
+ return inputs[0].getAttribute('capture') || 'environment';
89
+ }
90
+ return 'environment';
91
+ } catch (e) {
92
+ return 'environment';
93
+ }
94
+ })();
95
+ """
96
+ );
97
+ }
98
+
99
+ private static String captureDetectionHelpersJs() {
100
+ return """
101
+ var capgoNormalizeCapture = function(value) {
102
+ return value || 'environment';
103
+ };
104
+ var capgoIsCaptureFileInput = function(node) {
105
+ return node && node.tagName === 'INPUT' && node.type === 'file' && node.hasAttribute('capture');
106
+ };
107
+ var capgoFindCaptureFromClickEvent = function(e) {
108
+ if (!e) {
109
+ return null;
110
+ }
111
+ var path = e.composedPath ? e.composedPath() : null;
112
+ if (path && path.length) {
113
+ for (var i = 0; i < path.length; i++) {
114
+ var node = path[i];
115
+ if (capgoIsCaptureFileInput(node)) {
116
+ return capgoNormalizeCapture(node.getAttribute('capture'));
117
+ }
118
+ }
119
+ }
120
+ if (capgoIsCaptureFileInput(e.target)) {
121
+ return capgoNormalizeCapture(e.target.getAttribute('capture'));
122
+ }
123
+ return null;
124
+ };
125
+ var capgoFindCaptureFromActiveElementChain = function() {
126
+ var el = document.activeElement;
127
+ var depth = 0;
128
+ while (el && depth < 32) {
129
+ if (capgoIsCaptureFileInput(el)) {
130
+ return capgoNormalizeCapture(el.getAttribute('capture'));
131
+ }
132
+ if (el.shadowRoot) {
133
+ var inner = el.shadowRoot.activeElement;
134
+ if (inner) {
135
+ el = inner;
136
+ depth++;
137
+ continue;
138
+ }
139
+ var shadowInput = el.shadowRoot.querySelector('input[type="file"][capture]');
140
+ if (shadowInput) {
141
+ return capgoNormalizeCapture(shadowInput.getAttribute('capture'));
142
+ }
143
+ }
144
+ break;
145
+ }
146
+ return null;
147
+ };
148
+ """;
149
+ }
150
+ }
@@ -164,6 +164,7 @@ public class Options {
164
164
  private boolean disableGoBackOnNativeApplication;
165
165
  private boolean activeNativeNavigationForWebview;
166
166
  private boolean enableReloadGesture = false;
167
+ private boolean allowWebViewFullscreen = true;
167
168
  private boolean isPresentAfterPageLoad;
168
169
  private WebViewCallbacks callbacks;
169
170
  private PluginCall pluginCall;
@@ -649,6 +650,14 @@ public class Options {
649
650
  this.enableReloadGesture = enableReloadGesture;
650
651
  }
651
652
 
653
+ public boolean getAllowWebViewFullscreen() {
654
+ return allowWebViewFullscreen;
655
+ }
656
+
657
+ public void setAllowWebViewFullscreen(boolean allowWebViewFullscreen) {
658
+ this.allowWebViewFullscreen = allowWebViewFullscreen;
659
+ }
660
+
652
661
  public boolean getDisableGoBackOnNativeApplication() {
653
662
  return disableGoBackOnNativeApplication;
654
663
  }
@@ -879,6 +888,7 @@ public class Options {
879
888
  copy.setDisableGoBackOnNativeApplication(disableGoBackOnNativeApplication);
880
889
  copy.setActiveNativeNavigationForWebview(activeNativeNavigationForWebview);
881
890
  copy.setEnableReloadGesture(enableReloadGesture);
891
+ copy.setAllowWebViewFullscreen(allowWebViewFullscreen);
882
892
  copy.setPresentAfterPageLoad(false);
883
893
  copy.setVisibleTitle(VisibleTitle);
884
894
  copy.setToolbarColor(ToolbarColor);
@@ -22,6 +22,10 @@ final class WebViewCustomFullscreenSupport {
22
22
  return isActive;
23
23
  }
24
24
 
25
+ static boolean shouldRejectShow(boolean allowWebViewFullscreen, boolean isActive) {
26
+ return !allowWebViewFullscreen || shouldRejectDuplicateShow(isActive);
27
+ }
28
+
25
29
  static boolean shouldConsumeBackPress(boolean isActive) {
26
30
  return isActive;
27
31
  }
@@ -591,6 +591,9 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
591
591
  // Temporary URI for storing camera capture
592
592
  public Uri tempCameraUri;
593
593
 
594
+ // Last file input capture attribute reported from any frame (including cross-origin iframes).
595
+ private volatile String lastFileInputCaptureValue;
596
+
594
597
  public interface PermissionHandler {
595
598
  void handleCameraPermissionRequest(PermissionRequest request);
596
599
 
@@ -1061,6 +1064,11 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
1061
1064
  handleJavaScriptPostMessage(message);
1062
1065
  }
1063
1066
 
1067
+ @JavascriptInterface
1068
+ public void setFileInputCapture(String captureValue) {
1069
+ storeLastFileInputCaptureValue(captureValue == null || captureValue.isEmpty() ? "environment" : captureValue);
1070
+ }
1071
+
1064
1072
  @JavascriptInterface
1065
1073
  public void close() {
1066
1074
  try {
@@ -2505,6 +2513,8 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2505
2513
 
2506
2514
  _webView.setWebChromeClient(
2507
2515
  new WebChromeClient() {
2516
+ private Bitmap transparentVideoPoster;
2517
+
2508
2518
  @Override
2509
2519
  public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
2510
2520
  if (consoleMessage != null && _options != null && _options.getCaptureConsoleLogs() && _options.getCallbacks() != null) {
@@ -2521,6 +2531,15 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2521
2531
  return super.onConsoleMessage(consoleMessage);
2522
2532
  }
2523
2533
 
2534
+ // Android WebView otherwise paints a stretched grey play icon on <video> without poster or frames; Chrome paints nothing.
2535
+ @Override
2536
+ public Bitmap getDefaultVideoPoster() {
2537
+ if (transparentVideoPoster == null) {
2538
+ transparentVideoPoster = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
2539
+ }
2540
+ return transparentVideoPoster;
2541
+ }
2542
+
2524
2543
  // Enable HTML5/iframe fullscreen (e.g. embedded YouTube player fullscreen button).
2525
2544
  @Override
2526
2545
  public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
@@ -2562,140 +2581,36 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
2562
2581
  beginFileChooserRequest(filePathCallback, acceptTypes, isMultiple);
2563
2582
  final FileChooserRequestSupport.FileChooserRequest request = activeFileChooserRequest;
2564
2583
 
2565
- // Direct check for capture attribute in URL (fallback method)
2566
- boolean isCaptureInUrl;
2567
- String captureMode;
2568
- String currentUrl = getUrl();
2584
+ final boolean imageOnlyAcceptTypes = FileChooserAcceptSupport.isImageOnlyAcceptTypes(acceptTypes);
2585
+ final boolean captureEnabled = fileChooserParams.isCaptureEnabled();
2586
+ final String nativeCaptureForRequest = consumeLastFileInputCaptureValue();
2569
2587
 
2570
- // Look for capture in URL parameters - sometimes the attribute shows up in URL
2571
- if (currentUrl != null && currentUrl.contains("capture=")) {
2572
- isCaptureInUrl = true;
2573
- captureMode = currentUrl.contains("capture=user") ? "user" : "environment";
2574
- Log.d("InAppBrowser", "Found capture in URL: " + captureMode);
2575
- } else {
2576
- captureMode = null;
2577
- isCaptureInUrl = false;
2588
+ if (!FileInputCaptureChooserSupport.usesCameraCapturePath(imageOnlyAcceptTypes, captureEnabled)) {
2589
+ clearFileInputCapturePageState(webView);
2590
+ openFileChooser(request);
2591
+ return true;
2578
2592
  }
2579
2593
 
2580
- // For image-only inputs, try to detect capture attribute using JavaScript.
2581
- // Mixed accept lists (e.g. "image/*,application/pdf") must skip the camera
2582
- // path entirely — the camera can only produce images.
2583
- if (FileChooserAcceptSupport.isImageOnlyAcceptTypes(acceptTypes)) {
2584
- // Check if HTML content contains capture attribute on file inputs (synchronous check)
2585
- webView.evaluateJavascript(
2586
- "document.querySelector('input[type=\"file\"][capture]') !== null",
2587
- (hasCaptureValue) -> {
2588
- Log.d("InAppBrowser", "Quick capture check: " + hasCaptureValue);
2589
- if (Boolean.parseBoolean(hasCaptureValue.replace("\"", ""))) {
2590
- Log.d("InAppBrowser", "Found capture attribute in quick check");
2591
- }
2592
- }
2593
- );
2594
-
2595
- // Fixed JavaScript with proper error handling
2596
- String js = """
2597
- (function() {
2598
- try {
2599
- var captureAttr = null;
2600
- // Check active element first
2601
- if (document.activeElement &&
2602
- document.activeElement.tagName === 'INPUT' &&
2603
- document.activeElement.type === 'file') {
2604
- if (document.activeElement.hasAttribute('capture')) {
2605
- captureAttr = document.activeElement.getAttribute('capture') || 'environment';
2606
- return captureAttr;
2607
- }
2608
- }
2609
- // Try to find any input with capture attribute
2610
- var inputs = document.querySelectorAll('input[type="file"][capture]');
2611
- if (inputs && inputs.length > 0) {
2612
- captureAttr = inputs[0].getAttribute('capture') || 'environment';
2613
- return captureAttr;
2614
- }
2615
- // Try to extract from HTML attributes
2616
- var allInputs = document.getElementsByTagName('input');
2617
- for (var i = 0; i < allInputs.length; i++) {
2618
- var input = allInputs[i];
2619
- if (input.type === 'file') {
2620
- if (input.hasAttribute('capture')) {
2621
- captureAttr = input.getAttribute('capture') || 'environment';
2622
- return captureAttr;
2623
- }
2624
- // Look for the accept attribute containing image/* as this might be a camera input
2625
- var acceptAttr = input.getAttribute('accept');
2626
- if (acceptAttr && acceptAttr.indexOf('image/*') >= 0) {
2627
- console.log('Found input with image/* accept');
2628
- }
2629
- }
2630
- }
2631
- return '';
2632
- } catch(e) {
2633
- console.error('Capture detection error:', e);
2634
- return '';
2635
- }
2636
- })();
2637
- """;
2638
-
2639
- webView.evaluateJavascript(js, (value) -> {
2640
- if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2641
- return;
2642
- }
2643
-
2644
- Log.d("InAppBrowser", "Capture attribute JS result: " + value);
2645
-
2646
- // If we already found capture in URL, use that directly
2647
- if (isCaptureInUrl) {
2648
- Log.d("InAppBrowser", "Using capture from URL: " + captureMode);
2649
- launchCamera(captureMode.equals("user"), request);
2650
- return;
2651
- }
2652
-
2653
- // Process JavaScript result
2654
- if (value != null && value.length() > 2) {
2655
- // Clean up the value (remove quotes)
2656
- String captureValue = value.replace("\"", "");
2657
- Log.d("InAppBrowser", "Found capture attribute: " + captureValue);
2658
-
2659
- if (!captureValue.isEmpty()) {
2660
- activity.runOnUiThread(() -> launchCamera(captureValue.equals("user"), request));
2661
- return;
2662
- }
2663
- }
2664
-
2665
- // Look for hints in the web page source
2666
- Log.d("InAppBrowser", "Looking for camera hints in page content");
2667
- webView.evaluateJavascript("(function() { return document.documentElement.innerHTML; })()", (htmlSource) -> {
2668
- if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2669
- return;
2670
- }
2671
-
2672
- if (htmlSource != null && htmlSource.length() > 10) {
2673
- boolean hasCameraOrSelfieKeyword =
2674
- htmlSource.contains("capture=") || htmlSource.contains("camera") || htmlSource.contains("selfie");
2675
-
2676
- Log.d("InAppBrowser", "Page contains camera keywords: " + hasCameraOrSelfieKeyword);
2677
-
2678
- if (
2679
- hasCameraOrSelfieKeyword &&
2680
- currentUrl != null &&
2681
- (currentUrl.contains("selfie") || currentUrl.contains("camera") || currentUrl.contains("photo"))
2682
- ) {
2683
- Log.d("InAppBrowser", "URL suggests camera usage, launching camera");
2684
- activity.runOnUiThread(() -> launchCamera(currentUrl.contains("selfie"), request));
2685
- return;
2686
- }
2687
- }
2688
-
2689
- // If all detection methods fail, fall back to regular file picker
2690
- Log.d("InAppBrowser", "No capture attribute detected, using file picker");
2691
- openFileChooser(request);
2692
- });
2693
- });
2594
+ // For image-only inputs with capture enabled, launch the camera when possible.
2595
+ // Mixed accept lists (e.g. "image/*,application/pdf") use the regular picker above.
2596
+ if (nativeCaptureForRequest != null && !nativeCaptureForRequest.isEmpty()) {
2597
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2598
+ return true;
2599
+ }
2600
+ boolean useFrontCameraFromNative = nativeCaptureForRequest.equalsIgnoreCase("user");
2601
+ activity.runOnUiThread(() -> launchCamera(useFrontCameraFromNative, request));
2694
2602
  return true;
2695
2603
  }
2696
2604
 
2697
- // For non-image types, use regular file picker
2698
- openFileChooser(request);
2605
+ String js = FileInputCaptureChooserSupport.createChooserFallbackCaptureResolutionScript();
2606
+
2607
+ webView.evaluateJavascript(js, (value) -> {
2608
+ if (!FileChooserRequestSupport.isActive(request, activeFileChooserRequest)) {
2609
+ return;
2610
+ }
2611
+ boolean useFrontCamera = value != null && value.replace("\"", "").equalsIgnoreCase("user");
2612
+ activity.runOnUiThread(() -> launchCamera(useFrontCamera, request));
2613
+ });
2699
2614
  return true;
2700
2615
  }
2701
2616
 
@@ -3284,7 +3199,8 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3284
3199
 
3285
3200
  // Shows a native fullscreen view requested via the HTML5 Fullscreen API (e.g. embedded YouTube).
3286
3201
  private void showCustomFullscreenView(View view, WebChromeClient.CustomViewCallback callback) {
3287
- if (WebViewCustomFullscreenSupport.shouldRejectDuplicateShow(customFullscreenView != null)) {
3202
+ boolean allowWebViewFullscreen = _options == null || _options.getAllowWebViewFullscreen();
3203
+ if (WebViewCustomFullscreenSupport.shouldRejectShow(allowWebViewFullscreen, customFullscreenView != null)) {
3288
3204
  callback.onCustomViewHidden();
3289
3205
  return;
3290
3206
  }
@@ -3571,6 +3487,10 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3571
3487
  configureBlankToolbarLayout();
3572
3488
  }
3573
3489
 
3490
+ if (toolbarView != null && toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout) {
3491
+ removeAppBarShadow(appBarLayout);
3492
+ }
3493
+
3574
3494
  // Special handling for Android 15+
3575
3495
  if (isAndroid15Plus && !isBlankToolbar) {
3576
3496
  refreshEdgeToEdgeChrome();
@@ -3828,6 +3748,13 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3828
3748
  statusBarColorView.requestLayout();
3829
3749
  }
3830
3750
 
3751
+ // AppBarLayout re-applies its elevation through the StateListAnimator, so clearing the elevation alone is not enough.
3752
+ private static void removeAppBarShadow(com.google.android.material.appbar.AppBarLayout appBarLayout) {
3753
+ appBarLayout.setElevation(0);
3754
+ appBarLayout.setStateListAnimator(null);
3755
+ appBarLayout.setOutlineProvider(null);
3756
+ }
3757
+
3831
3758
  private void refreshEdgeToEdgeChrome() {
3832
3759
  if (_webView == null || _options == null) {
3833
3760
  return;
@@ -3840,9 +3767,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
3840
3767
  return;
3841
3768
  }
3842
3769
 
3843
- appBarLayout.setElevation(0);
3844
- appBarLayout.setStateListAnimator(null);
3845
- appBarLayout.setOutlineProvider(null);
3770
+ removeAppBarShadow(appBarLayout);
3846
3771
 
3847
3772
  final int finalBgColor = resolveWindowBackgroundColor();
3848
3773
  _webView.post(() -> {
@@ -4392,6 +4317,23 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
4392
4317
  _webView.post(() -> callback.onError(message));
4393
4318
  }
4394
4319
 
4320
+ private synchronized void storeLastFileInputCaptureValue(String captureValue) {
4321
+ lastFileInputCaptureValue = captureValue;
4322
+ }
4323
+
4324
+ private synchronized String consumeLastFileInputCaptureValue() {
4325
+ String value = lastFileInputCaptureValue;
4326
+ lastFileInputCaptureValue = null;
4327
+ return value;
4328
+ }
4329
+
4330
+ private void clearFileInputCapturePageState(WebView webView) {
4331
+ if (webView == null) {
4332
+ return;
4333
+ }
4334
+ webView.evaluateJavascript("(function(){ window.__capgoLastFileCapture = null; })();", null);
4335
+ }
4336
+
4395
4337
  private void injectJavaScriptInterface() {
4396
4338
  if (_webView == null) {
4397
4339
  Log.w("InAppBrowser", "Cannot inject JavaScript interface - WebView is null");
@@ -4405,6 +4347,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
4405
4347
  if (_webView != null) {
4406
4348
  try {
4407
4349
  _webView.evaluateJavascript(script, null);
4350
+ injectFileInputCaptureHookScript();
4408
4351
  injectBlankTargetInCurrentWebViewScript();
4409
4352
  } catch (Exception e) {
4410
4353
  Log.e("InAppBrowser", "Error injecting JavaScript interface: " + e.getMessage());
@@ -4416,6 +4359,17 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
4416
4359
  }
4417
4360
  }
4418
4361
 
4362
+ private void injectFileInputCaptureHookScript() {
4363
+ if (_webView == null) {
4364
+ return;
4365
+ }
4366
+ _webView.evaluateJavascript(createFileInputCaptureHookScript(), null);
4367
+ }
4368
+
4369
+ private String createFileInputCaptureHookScript() {
4370
+ return FileInputCaptureChooserSupport.createFileInputCaptureHookScript();
4371
+ }
4372
+
4419
4373
  private void injectDocumentStartJavaScriptInterface() {
4420
4374
  if (_webView == null) {
4421
4375
  Log.w("InAppBrowser", "Cannot inject document-start JavaScript interface - WebView is null");
@@ -4428,6 +4382,7 @@ public class WebViewDialog extends ComponentDialog implements ProxyResponseRouti
4428
4382
 
4429
4383
  try {
4430
4384
  injectDocumentStartPostMessageBridge();
4385
+ WebViewCompat.addDocumentStartJavaScript(_webView, createFileInputCaptureHookScript(), Collections.singleton("*"));
4431
4386
  WebViewCompat.addDocumentStartJavaScript(_webView, createMobileAppBridgeScript(), Collections.singleton("*"));
4432
4387
  // Honor preShowScriptInjectionTime: "documentStart" (matches iOS WKUserScript
4433
4388
  // .atDocumentStart). Runs before page scripts and persists across navigations,
package/dist/docs.json CHANGED
@@ -1958,6 +1958,26 @@
1958
1958
  "complexTypes": [],
1959
1959
  "type": "boolean | undefined"
1960
1960
  },
1961
+ {
1962
+ "name": "allowWebViewFullscreen",
1963
+ "tags": [
1964
+ {
1965
+ "text": "8.22.0",
1966
+ "name": "since"
1967
+ },
1968
+ {
1969
+ "text": "true",
1970
+ "name": "default"
1971
+ },
1972
+ {
1973
+ "text": "allowWebViewFullscreen: false",
1974
+ "name": "example"
1975
+ }
1976
+ ],
1977
+ "docs": "Allow pages to enter HTML5 fullscreen (e.g. `requestFullscreen()` or a video player's fullscreen button).\nWhen `false`, fullscreen requests are rejected and the toolbar stays visible, so the user can always close the browser.\n\nAndroid only. On iOS, element fullscreen is not enabled by the plugin, so this option has no effect.",
1978
+ "complexTypes": [],
1979
+ "type": "boolean | undefined"
1980
+ },
1961
1981
  {
1962
1982
  "name": "disableGoBackOnNativeApplication",
1963
1983
  "tags": [
@@ -996,6 +996,18 @@ export interface OpenWebViewOptions {
996
996
  * enableReloadGesture: true
997
997
  */
998
998
  enableReloadGesture?: boolean;
999
+ /**
1000
+ * Allow pages to enter HTML5 fullscreen (e.g. `requestFullscreen()` or a video player's fullscreen button).
1001
+ * When `false`, fullscreen requests are rejected and the toolbar stays visible, so the user can always close the browser.
1002
+ *
1003
+ * Android only. On iOS, element fullscreen is not enabled by the plugin, so this option has no effect.
1004
+ *
1005
+ * @since 8.22.0
1006
+ * @default true
1007
+ * @example
1008
+ * allowWebViewFullscreen: false
1009
+ */
1010
+ allowWebViewFullscreen?: boolean;
999
1011
  /**
1000
1012
  * Disable the possibility to go back on native application,
1001
1013
  * useful to force user to stay on the webview, Android only