@capgo/capacitor-inappbrowser 8.13.3 → 8.13.5

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.
@@ -0,0 +1,41 @@
1
+ package ee.forgr.capacitor_inappbrowser;
2
+
3
+ /**
4
+ * Helpers for deciding when a managed WebView dialog must relayout after a configuration
5
+ * change. Capacitor activities declare {@code android:configChanges}, so the host activity
6
+ * (and this dialog) are not recreated on rotation — layouts and window insets must be
7
+ * refreshed explicitly or the WebView can stop scrolling after portrait→landscape.
8
+ */
9
+ final class OrientationLayoutSupport {
10
+
11
+ private OrientationLayoutSupport() {}
12
+
13
+ static boolean shouldRefreshBrowserLayout(
14
+ Integer previousOrientation,
15
+ Integer previousScreenWidthDp,
16
+ Integer previousScreenHeightDp,
17
+ Integer previousSmallestScreenWidthDp,
18
+ Integer previousDensityDpi,
19
+ int currentOrientation,
20
+ int currentScreenWidthDp,
21
+ int currentScreenHeightDp,
22
+ int currentSmallestScreenWidthDp,
23
+ int currentDensityDpi
24
+ ) {
25
+ if (previousOrientation == null) {
26
+ return true;
27
+ }
28
+
29
+ return (
30
+ previousOrientation != currentOrientation ||
31
+ previousScreenWidthDp == null ||
32
+ previousScreenWidthDp != currentScreenWidthDp ||
33
+ previousScreenHeightDp == null ||
34
+ previousScreenHeightDp != currentScreenHeightDp ||
35
+ previousSmallestScreenWidthDp == null ||
36
+ previousSmallestScreenWidthDp != currentSmallestScreenWidthDp ||
37
+ previousDensityDpi == null ||
38
+ previousDensityDpi != currentDensityDpi
39
+ );
40
+ }
41
+ }
@@ -97,18 +97,19 @@ final class SafeAreaInsetsSupport {
97
97
 
98
98
  /**
99
99
  * Bottom padding to apply to the WebView container (a SwipeRefreshLayout, which honours its own
100
- * padding but ignores child margins). Combines the resolved safe bottom inset with a compensation
101
- * term: on Android 15+ the AppBarLayout is pushed down by the status-bar height, which shifts the
102
- * container's bottom edge off-screen by that same amount, so it must be added back as padding
103
- * regardless of the safe-margin option.
100
+ * padding but ignores child margins). Takes the larger of the safe bottom inset and the keyboard
101
+ * (IME) inset via {@link #resolveBottomMargin}, then adds a compensation term: on Android 15+ the
102
+ * AppBarLayout is pushed down by the status-bar height, which shifts the container's bottom edge
103
+ * off-screen by that same amount, so it must be added back as padding regardless of the options.
104
104
  */
105
105
  static int resolveContainerBottomPadding(
106
106
  boolean enabledSafeBottomMargin,
107
107
  int safeBottomInset,
108
+ int imeBottom,
108
109
  boolean appBarHandlesTopInset,
109
110
  int statusBarTop
110
111
  ) {
111
- int base = enabledSafeBottomMargin ? Math.max(0, safeBottomInset) : 0;
112
+ int base = resolveBottomMargin(enabledSafeBottomMargin, safeBottomInset, imeBottom);
112
113
  int appBarCompensation = appBarHandlesTopInset ? Math.max(0, statusBarTop) : 0;
113
114
  return base + appBarCompensation;
114
115
  }
@@ -130,4 +131,14 @@ final class SafeAreaInsetsSupport {
130
131
 
131
132
  return Math.max(0, statusBarTop);
132
133
  }
134
+
135
+ /**
136
+ * Whether the WebView container must be inset for the bottom system bar. On Android 15+ the dialog
137
+ * is forced edge-to-edge (decorFitsSystemWindows=false), so the window always draws under the
138
+ * navigation bar and insetting is mandatory to keep bottom content on-screen, regardless of the
139
+ * enabledSafeBottomMargin opt-in that older versions honour.
140
+ */
141
+ static boolean shouldInsetBottomForContainer(boolean enabledSafeBottomMargin, boolean isEdgeToEdge) {
142
+ return enabledSafeBottomMargin || isEdgeToEdge;
143
+ }
133
144
  }
@@ -5,10 +5,12 @@ import android.app.Activity;
5
5
  import android.app.AlertDialog;
6
6
  import android.app.Dialog;
7
7
  import android.content.ActivityNotFoundException;
8
+ import android.content.ComponentCallbacks2;
8
9
  import android.content.Context;
9
10
  import android.content.DialogInterface;
10
11
  import android.content.Intent;
11
12
  import android.content.res.AssetManager;
13
+ import android.content.res.Configuration;
12
14
  import android.content.res.Resources;
13
15
  import android.graphics.Bitmap;
14
16
  import android.graphics.Canvas;
@@ -356,6 +358,20 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
356
358
  private boolean cachedTitleIconResolved;
357
359
  private boolean isHiddenModeActive = false;
358
360
  private boolean toolbarHideInProgress = false;
361
+ private boolean configurationCallbacksRegistered = false;
362
+ private Configuration lastConfiguration;
363
+ private final ComponentCallbacks2 configurationCallbacks = new ComponentCallbacks2() {
364
+ @Override
365
+ public void onConfigurationChanged(Configuration newConfig) {
366
+ handleConfigurationChanged(newConfig);
367
+ }
368
+
369
+ @Override
370
+ public void onLowMemory() {}
371
+
372
+ @Override
373
+ public void onTrimMemory(int level) {}
374
+ };
359
375
  private WindowManager.LayoutParams previousWindowAttributes;
360
376
  private Drawable previousWindowBackground;
361
377
  private ViewGroup.LayoutParams previousWebViewLayoutParams;
@@ -2824,6 +2840,9 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2824
2840
  showAccordingToLayerModeOrFallback();
2825
2841
  resolveOpenWebViewIfNeeded();
2826
2842
  }
2843
+
2844
+ // Capacitor activities handle orientation themselves; refresh dialog layout explicitly.
2845
+ registerConfigurationCallbacks();
2827
2846
  }
2828
2847
 
2829
2848
  private void applyHiddenMode() {
@@ -2974,22 +2993,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2974
2993
  // Check if we need Android 15+ specific fixes
2975
2994
  boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
2976
2995
 
2977
- // Find status bar color view and toolbar for Android 15+ specific handling
2978
- View statusBarColorView = findViewById(R.id.status_bar_color_view);
2979
2996
  View toolbarView = findViewById(R.id.tool_bar);
2980
2997
 
2981
2998
  // Fix content browser layout height for all Android versions to allow proper scrolling
2982
2999
  // This fixes landscape scrolling issues where bottom content is unreachable
2983
- View contentBrowserLayout = findViewById(R.id.content_browser_layout);
2984
- if (contentBrowserLayout != null) {
2985
- ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
2986
- if (layoutParams != null) {
2987
- // Use MATCH_PARENT for height to allow proper scrolling in all orientations
2988
- // The AppBarLayout's layout_behavior will handle positioning automatically
2989
- layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
2990
- contentBrowserLayout.setLayoutParams(layoutParams);
2991
- }
2992
- }
3000
+ ensureContentBrowserMatchParentHeight();
2993
3001
 
2994
3002
  boolean isBlankToolbar = _options != null && TextUtils.equals(_options.getToolbarType(), "blank");
2995
3003
  if (isBlankToolbar) {
@@ -2998,53 +3006,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
2998
3006
 
2999
3007
  // Special handling for Android 15+
3000
3008
  if (isAndroid15Plus && !isBlankToolbar) {
3001
- // Get AppBarLayout which contains the toolbar
3002
- if (toolbarView != null && toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout) {
3003
- // Remove elevation to eliminate shadows (only on Android 15+)
3004
- appBarLayout.setElevation(0);
3005
- appBarLayout.setStateListAnimator(null);
3006
- appBarLayout.setOutlineProvider(null);
3007
-
3008
- // Determine background color to use
3009
- int backgroundColor = Color.BLACK; // Default fallback
3010
- if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
3011
- try {
3012
- backgroundColor = Color.parseColor(_options.getToolbarColor());
3013
- } catch (IllegalArgumentException e) {
3014
- Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
3015
- }
3016
- } else {
3017
- // Follow system theme if no color specified
3018
- boolean isDarkTheme = isDarkThemeEnabled();
3019
- backgroundColor = isDarkTheme ? Color.BLACK : Color.WHITE;
3020
- }
3021
-
3022
- // Apply fixes for Android 15+ using a delayed post
3023
- final int finalBgColor = backgroundColor;
3024
- _webView.post(() -> {
3025
- // Get status bar height
3026
- int statusBarHeight = 0;
3027
- int resourceId = getContext().getResources().getIdentifier("status_bar_height", "dimen", "android");
3028
- if (resourceId > 0) {
3029
- statusBarHeight = getContext().getResources().getDimensionPixelSize(resourceId);
3030
- }
3031
-
3032
- // Fix status bar view
3033
- if (statusBarColorView != null) {
3034
- ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
3035
- params.height = statusBarHeight;
3036
- statusBarColorView.setLayoutParams(params);
3037
- statusBarColorView.setBackgroundColor(finalBgColor);
3038
- statusBarColorView.setVisibility(View.VISIBLE);
3039
- }
3040
-
3041
- // Fix AppBarLayout position
3042
- ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) appBarLayout.getLayoutParams();
3043
- params.topMargin = statusBarHeight;
3044
- appBarLayout.setLayoutParams(params);
3045
- appBarLayout.setBackgroundColor(finalBgColor);
3046
- });
3047
- }
3009
+ refreshEdgeToEdgeChrome();
3048
3010
  }
3049
3011
 
3050
3012
  // Resolve insets from the dialog window root; layout children can receive already-fitted zero insets.
@@ -3152,6 +3114,232 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
3152
3114
  mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 1200);
3153
3115
  }
3154
3116
 
3117
+ private void registerConfigurationCallbacks() {
3118
+ if (configurationCallbacksRegistered) {
3119
+ return;
3120
+ }
3121
+
3122
+ Context callbackContext = activity != null ? activity : _context;
3123
+ if (callbackContext == null) {
3124
+ return;
3125
+ }
3126
+
3127
+ lastConfiguration = new Configuration(callbackContext.getResources().getConfiguration());
3128
+ callbackContext.registerComponentCallbacks(configurationCallbacks);
3129
+ configurationCallbacksRegistered = true;
3130
+ }
3131
+
3132
+ private void unregisterConfigurationCallbacks() {
3133
+ if (!configurationCallbacksRegistered) {
3134
+ return;
3135
+ }
3136
+
3137
+ Context callbackContext = activity != null ? activity : _context;
3138
+ if (callbackContext != null) {
3139
+ try {
3140
+ callbackContext.unregisterComponentCallbacks(configurationCallbacks);
3141
+ } catch (Exception e) {
3142
+ Log.w("InAppBrowser", "Failed to unregister configuration callbacks: " + e.getMessage());
3143
+ }
3144
+ }
3145
+
3146
+ configurationCallbacksRegistered = false;
3147
+ lastConfiguration = null;
3148
+ }
3149
+
3150
+ private void handleConfigurationChanged(Configuration newConfig) {
3151
+ if (isDismissing || _webView == null) {
3152
+ return;
3153
+ }
3154
+
3155
+ Integer previousOrientation = lastConfiguration != null ? lastConfiguration.orientation : null;
3156
+ Integer previousScreenWidthDp = lastConfiguration != null ? lastConfiguration.screenWidthDp : null;
3157
+ Integer previousScreenHeightDp = lastConfiguration != null ? lastConfiguration.screenHeightDp : null;
3158
+ Integer previousSmallestScreenWidthDp = lastConfiguration != null ? lastConfiguration.smallestScreenWidthDp : null;
3159
+ Integer previousDensityDpi = lastConfiguration != null ? lastConfiguration.densityDpi : null;
3160
+
3161
+ int currentOrientation = newConfig != null ? newConfig.orientation : (previousOrientation != null ? previousOrientation : 0);
3162
+ int currentScreenWidthDp =
3163
+ newConfig != null ? newConfig.screenWidthDp : (previousScreenWidthDp != null ? previousScreenWidthDp : 0);
3164
+ int currentScreenHeightDp =
3165
+ newConfig != null ? newConfig.screenHeightDp : (previousScreenHeightDp != null ? previousScreenHeightDp : 0);
3166
+ int currentSmallestScreenWidthDp =
3167
+ newConfig != null
3168
+ ? newConfig.smallestScreenWidthDp
3169
+ : (previousSmallestScreenWidthDp != null ? previousSmallestScreenWidthDp : 0);
3170
+ int currentDensityDpi = newConfig != null ? newConfig.densityDpi : (previousDensityDpi != null ? previousDensityDpi : 0);
3171
+
3172
+ boolean shouldRefresh = OrientationLayoutSupport.shouldRefreshBrowserLayout(
3173
+ previousOrientation,
3174
+ previousScreenWidthDp,
3175
+ previousScreenHeightDp,
3176
+ previousSmallestScreenWidthDp,
3177
+ previousDensityDpi,
3178
+ currentOrientation,
3179
+ currentScreenWidthDp,
3180
+ currentScreenHeightDp,
3181
+ currentSmallestScreenWidthDp,
3182
+ currentDensityDpi
3183
+ );
3184
+
3185
+ if (newConfig != null) {
3186
+ lastConfiguration = new Configuration(newConfig);
3187
+ }
3188
+
3189
+ if (!shouldRefresh) {
3190
+ return;
3191
+ }
3192
+
3193
+ if (Looper.myLooper() == Looper.getMainLooper()) {
3194
+ refreshLayoutForConfigurationChange();
3195
+ } else {
3196
+ mainHandler.post(this::refreshLayoutForConfigurationChange);
3197
+ }
3198
+ }
3199
+
3200
+ /**
3201
+ * Capacitor keeps the host activity alive across orientation changes. Re-apply fullscreen
3202
+ * window dimensions, system chrome, and safe-area insets so the WebView stays scrollable
3203
+ * after portrait↔landscape transitions.
3204
+ */
3205
+ private void refreshLayoutForConfigurationChange() {
3206
+ if (isDismissing || _webView == null) {
3207
+ return;
3208
+ }
3209
+
3210
+ if (isHiddenModeActive) {
3211
+ requestSafeAreaInsets();
3212
+ return;
3213
+ }
3214
+
3215
+ if (backLayerActive) {
3216
+ applyBackLayerDimensions();
3217
+ } else {
3218
+ applyDimensions();
3219
+ }
3220
+
3221
+ ensureContentBrowserMatchParentHeight();
3222
+
3223
+ boolean isBlankToolbar = _options != null && TextUtils.equals(_options.getToolbarType(), "blank");
3224
+ if (isBlankToolbar) {
3225
+ configureBlankToolbarLayout();
3226
+ } else if (Build.VERSION.SDK_INT >= 35) {
3227
+ refreshEdgeToEdgeChrome();
3228
+ } else {
3229
+ refreshStatusBarColorViewHeight();
3230
+ }
3231
+
3232
+ View coordinator = findViewById(R.id.coordinator_layout);
3233
+ if (coordinator != null) {
3234
+ coordinator.requestLayout();
3235
+ }
3236
+
3237
+ if (swipeRefreshLayout != null) {
3238
+ swipeRefreshLayout.requestLayout();
3239
+ }
3240
+
3241
+ _webView.requestLayout();
3242
+ _webView.invalidate();
3243
+ requestSafeAreaInsets();
3244
+
3245
+ mainHandler.post(this::applyContainerInsetsSnapshot);
3246
+ mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 100);
3247
+ mainHandler.postDelayed(this::applyContainerInsetsSnapshot, 300);
3248
+
3249
+ _webView.post(() -> {
3250
+ if (_webView == null) {
3251
+ return;
3252
+ }
3253
+ _webView.evaluateJavascript("(function(){window.dispatchEvent(new Event('resize'));})();", null);
3254
+ });
3255
+ }
3256
+
3257
+ private void ensureContentBrowserMatchParentHeight() {
3258
+ View contentBrowserLayout = findViewById(R.id.content_browser_layout);
3259
+ if (contentBrowserLayout == null) {
3260
+ return;
3261
+ }
3262
+
3263
+ ViewGroup.LayoutParams layoutParams = contentBrowserLayout.getLayoutParams();
3264
+ if (layoutParams == null) {
3265
+ return;
3266
+ }
3267
+
3268
+ layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
3269
+ contentBrowserLayout.setLayoutParams(layoutParams);
3270
+ }
3271
+
3272
+ private void refreshStatusBarColorViewHeight() {
3273
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
3274
+ if (statusBarColorView == null) {
3275
+ return;
3276
+ }
3277
+
3278
+ int statusBarHeight = getSystemStatusBarHeight();
3279
+ ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
3280
+ if (params == null) {
3281
+ return;
3282
+ }
3283
+
3284
+ params.height = statusBarHeight;
3285
+ statusBarColorView.setLayoutParams(params);
3286
+ statusBarColorView.requestLayout();
3287
+ }
3288
+
3289
+ private void refreshEdgeToEdgeChrome() {
3290
+ if (_webView == null || _options == null) {
3291
+ return;
3292
+ }
3293
+
3294
+ View statusBarColorView = findViewById(R.id.status_bar_color_view);
3295
+ View toolbarView = findViewById(R.id.tool_bar);
3296
+ if (toolbarView == null || !(toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout appBarLayout)) {
3297
+ refreshStatusBarColorViewHeight();
3298
+ return;
3299
+ }
3300
+
3301
+ appBarLayout.setElevation(0);
3302
+ appBarLayout.setStateListAnimator(null);
3303
+ appBarLayout.setOutlineProvider(null);
3304
+
3305
+ int backgroundColor = Color.BLACK;
3306
+ if (_options.getToolbarColor() != null && !_options.getToolbarColor().isEmpty()) {
3307
+ try {
3308
+ backgroundColor = Color.parseColor(_options.getToolbarColor());
3309
+ } catch (IllegalArgumentException e) {
3310
+ Log.e("InAppBrowser", "Invalid toolbar color, using black: " + e.getMessage());
3311
+ }
3312
+ } else {
3313
+ backgroundColor = isDarkThemeEnabled() ? Color.BLACK : Color.WHITE;
3314
+ }
3315
+
3316
+ final int finalBgColor = backgroundColor;
3317
+ _webView.post(() -> {
3318
+ if (_webView == null) {
3319
+ return;
3320
+ }
3321
+
3322
+ int statusBarHeight = getSystemStatusBarHeight();
3323
+
3324
+ if (statusBarColorView != null) {
3325
+ ViewGroup.LayoutParams params = statusBarColorView.getLayoutParams();
3326
+ if (params != null) {
3327
+ params.height = statusBarHeight;
3328
+ statusBarColorView.setLayoutParams(params);
3329
+ }
3330
+ statusBarColorView.setBackgroundColor(finalBgColor);
3331
+ statusBarColorView.setVisibility(View.VISIBLE);
3332
+ }
3333
+
3334
+ ViewGroup.LayoutParams appBarParams = appBarLayout.getLayoutParams();
3335
+ if (appBarParams instanceof ViewGroup.MarginLayoutParams marginParams) {
3336
+ marginParams.topMargin = statusBarHeight;
3337
+ appBarLayout.setLayoutParams(marginParams);
3338
+ }
3339
+ appBarLayout.setBackgroundColor(finalBgColor);
3340
+ });
3341
+ }
3342
+
3155
3343
  private void applySafeAreaMargins(
3156
3344
  Insets bars,
3157
3345
  Insets navigationBars,
@@ -3227,6 +3415,8 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
3227
3415
  Insets systemGestures = windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.systemGestures()) : Insets.NONE;
3228
3416
  Insets mandatoryGestures =
3229
3417
  windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.mandatorySystemGestures()) : Insets.NONE;
3418
+ Insets ime = windowInsets != null ? windowInsets.getInsets(WindowInsetsCompat.Type.ime()) : Insets.NONE;
3419
+ boolean keyboardVisible = windowInsets != null && windowInsets.isVisible(WindowInsetsCompat.Type.ime());
3230
3420
 
3231
3421
  boolean isAndroid15Plus = Build.VERSION.SDK_INT >= 35;
3232
3422
  View toolbarView = findViewById(R.id.tool_bar);
@@ -3237,8 +3427,10 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
3237
3427
  toolbarView.getVisibility() == View.VISIBLE &&
3238
3428
  toolbarView.getParent() instanceof com.google.android.material.appbar.AppBarLayout;
3239
3429
 
3430
+ boolean applyBottomInset = SafeAreaInsetsSupport.shouldInsetBottomForContainer(_options.getEnabledSafeMargin(), isAndroid15Plus);
3431
+
3240
3432
  int statusBarTop = bars.top > 0 ? bars.top : getSystemStatusBarHeight();
3241
- int fallbackBottomInset = _options.getEnabledSafeMargin() ? getSystemNavigationBarHeight() : 0;
3433
+ int fallbackBottomInset = applyBottomInset ? getSystemNavigationBarHeight() : 0;
3242
3434
  int safeBottomInset = SafeAreaInsetsSupport.resolveSafeBottomInsetWithFallback(
3243
3435
  bars.bottom,
3244
3436
  navigationBars.bottom,
@@ -3249,7 +3441,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
3249
3441
  navigationBars.left,
3250
3442
  navigationBars.right,
3251
3443
  fallbackBottomInset,
3252
- _options.getEnabledSafeMargin()
3444
+ applyBottomInset
3253
3445
  );
3254
3446
 
3255
3447
  int padTop = SafeAreaInsetsSupport.resolveContainerTopPadding(
@@ -3258,9 +3450,11 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
3258
3450
  statusBarTop,
3259
3451
  appBarHandlesTopInset
3260
3452
  );
3453
+ int imeBottom = SafeAreaInsetsSupport.resolveImeBottomInset(keyboardVisible, ime.bottom, isAndroid15Plus);
3261
3454
  int padBottom = SafeAreaInsetsSupport.resolveContainerBottomPadding(
3262
- _options.getEnabledSafeMargin(),
3455
+ applyBottomInset,
3263
3456
  safeBottomInset,
3457
+ imeBottom,
3264
3458
  appBarHandlesTopInset,
3265
3459
  statusBarTop
3266
3460
  );
@@ -5853,6 +6047,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
5853
6047
 
5854
6048
  @Override
5855
6049
  public void dismiss() {
6050
+ unregisterConfigurationCallbacks();
5856
6051
  scheduleHostWebViewInsetRestore();
5857
6052
  detachBackLayer();
5858
6053
 
@@ -6963,7 +7158,12 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6963
7158
  Integer x = _options.getX();
6964
7159
  Integer y = _options.getY();
6965
7160
 
6966
- WindowManager.LayoutParams params = getWindow().getAttributes();
7161
+ Window window = getWindow();
7162
+ if (window == null) {
7163
+ return;
7164
+ }
7165
+
7166
+ WindowManager.LayoutParams params = window.getAttributes();
6967
7167
 
6968
7168
  // If both width and height are specified, use custom dimensions
6969
7169
  if (width != null && height != null) {
@@ -6973,6 +7173,7 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6973
7173
  params.x = (x != null) ? (int) getPixels(x) : 0;
6974
7174
  params.y = (y != null) ? (int) getPixels(y) : 0;
6975
7175
  params.gravity = Gravity.TOP | Gravity.START;
7176
+ window.setAttributes(params);
6976
7177
  } else if (height != null && width == null) {
6977
7178
  // If only height is specified, use custom height with fullscreen width
6978
7179
  params.gravity = Gravity.TOP | Gravity.LEFT;
@@ -6981,15 +7182,16 @@ public class WebViewDialog extends Dialog implements ProxyResponseRouting.ProxyR
6981
7182
  params.x = 0;
6982
7183
  params.y = (y != null) ? (int) getPixels(y) : 0;
6983
7184
  params.gravity = Gravity.TOP | Gravity.START;
7185
+ window.setAttributes(params);
6984
7186
  } else {
6985
- // Default to fullscreen
6986
- params.width = WindowManager.LayoutParams.MATCH_PARENT;
6987
- params.height = WindowManager.LayoutParams.MATCH_PARENT;
7187
+ // Default to fullscreen. Prefer setLayout so MATCH_PARENT is not replaced by
7188
+ // previously resolved portrait/landscape pixel sizes after a configuration change.
6988
7189
  params.x = 0;
6989
7190
  params.y = 0;
7191
+ params.gravity = Gravity.TOP | Gravity.START;
7192
+ window.setAttributes(params);
7193
+ window.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
6990
7194
  }
6991
-
6992
- getWindow().setAttributes(params);
6993
7195
  }
6994
7196
 
6995
7197
  /**
@@ -110,6 +110,42 @@ enum CustomSchemeOpenSupport {
110
110
  }
111
111
  }
112
112
 
113
+ enum SecureWindowRedirectSupport {
114
+ /// The auth session reports any navigation on the callback scheme, so the redirect is
115
+ /// identified component-wise. Query items configured in the redirect URI are matched,
116
+ /// while any extra items (e.g. provider code, state) are ignored, unless they reuse a
117
+ /// configured name: parsers disagree over which duplicate takes precedence.
118
+ static func matches(_ callbackURL: URL, redirectUri: String) -> Bool {
119
+ guard let expected = URLComponents(string: redirectUri),
120
+ let received = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false),
121
+ received.scheme?.lowercased() == expected.scheme?.lowercased(),
122
+ received.percentEncodedUser == expected.percentEncodedUser,
123
+ received.percentEncodedPassword == expected.percentEncodedPassword,
124
+ received.percentEncodedHost?.lowercased() == expected.percentEncodedHost?.lowercased(),
125
+ received.port == expected.port,
126
+ normalizedPath(received.percentEncodedPath) == normalizedPath(expected.percentEncodedPath) else {
127
+ return false
128
+ }
129
+
130
+ let expectedItems = expected.percentEncodedQueryItems ?? []
131
+ let receivedItems = received.percentEncodedQueryItems ?? []
132
+ let configuredDecodedNames = Set((expected.queryItems ?? []).map(\.name))
133
+ let configuredReceivedItemCount = (received.queryItems ?? []).filter {
134
+ configuredDecodedNames.contains($0.name)
135
+ }.count
136
+ guard configuredReceivedItemCount == expectedItems.count else {
137
+ return false
138
+ }
139
+ return Set(expectedItems.map(\.name)).allSatisfy { name in
140
+ receivedItems.filter { $0.name == name } == expectedItems.filter { $0.name == name }
141
+ }
142
+ }
143
+
144
+ private static func normalizedPath(_ path: String) -> String {
145
+ path == "/" ? "" : path
146
+ }
147
+ }
148
+
113
149
  protocol ProxyRequestLocating {
114
150
  func hasPendingProxyRequest(_ requestId: String) -> Bool
115
151
  }
@@ -376,7 +412,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
376
412
  case aware = "AWARE"
377
413
  case fakeVisible = "FAKE_VISIBLE"
378
414
  }
379
- private let pluginVersion: String = "8.13.3"
415
+ private let pluginVersion: String = "8.13.5"
380
416
  public let identifier = "CapgoInAppBrowserPlugin"
381
417
  public let jsName = "CapgoInAppBrowser"
382
418
  public let pluginMethods: [CAPPluginMethod] = [
@@ -1268,7 +1304,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
1268
1304
  return
1269
1305
  }
1270
1306
  let titleFontFamily = call.getString("titleFontFamily")
1271
- let isInspectable = call.getBool("isInspectable", false)
1307
+ let isInspectable = call.getBool("isInspectable", self.bridge?.config.isWebDebuggable ?? false)
1272
1308
  let preventDeeplink = call.getBool("preventDeeplink", false)
1273
1309
  let openBlankTargetInWebView = call.getBool("openBlankTargetInWebView", false)
1274
1310
  let isAnimated = call.getBool("isAnimated", true)
@@ -2637,6 +2673,11 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
2637
2673
  return
2638
2674
  }
2639
2675
 
2676
+ guard let callbackURLScheme = URL(string: redirectUri)?.scheme else {
2677
+ call.reject("Invalid Redirect URI")
2678
+ return
2679
+ }
2680
+
2640
2681
  // Store the call for later resolution
2641
2682
  self.openSecureWindowCall = call
2642
2683
 
@@ -2644,7 +2685,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
2644
2685
 
2645
2686
  // Open the URL in a secure browser window
2646
2687
  DispatchQueue.main.async {
2647
- let session = ASWebAuthenticationSession(url: url, callbackURLScheme: url.scheme) {
2688
+ let session = ASWebAuthenticationSession(url: url, callbackURLScheme: callbackURLScheme) {
2648
2689
  callbackURL, error in
2649
2690
 
2650
2691
  // Clean up the stored call
@@ -2661,7 +2702,7 @@ public class CapgoInAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
2661
2702
  return
2662
2703
  }
2663
2704
 
2664
- if !callbackURL.absoluteString.hasPrefix(redirectUri) {
2705
+ if !SecureWindowRedirectSupport.matches(callbackURL, redirectUri: redirectUri) {
2665
2706
  call.reject("Redirect URI does not match, expected " + redirectUri + " but got " + callbackURL.absoluteString)
2666
2707
  return
2667
2708
  }
@@ -439,6 +439,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
439
439
  open var httpMethod: String?
440
440
  open var httpBody: String?
441
441
  open var capBrowserPlugin: CapgoInAppBrowserPlugin?
442
+ open var isInspectable: Bool = false
442
443
  var instanceId: String = ""
443
444
  var shareDisclaimer: [String: Any]?
444
445
  var shareSubject: String?
@@ -2032,11 +2033,12 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2032
2033
  }
2033
2034
  }
2034
2035
 
2035
- open func initWebview(isInspectable: Bool = true) {
2036
+ open func initWebview(isInspectable: Bool = false) {
2036
2037
  if self.isWebViewInitialized {
2037
2038
  return
2038
2039
  }
2039
2040
  self.isWebViewInitialized = true
2041
+ self.isInspectable = isInspectable
2040
2042
  self.startObservingKeyboardViewportChanges()
2041
2043
  self.view.backgroundColor = UIColor.white
2042
2044
 
@@ -2183,9 +2185,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2183
2185
  // }
2184
2186
 
2185
2187
  if #available(iOS 16.4, *) {
2186
- webView.isInspectable = true
2187
- } else {
2188
- // Fallback on earlier versions
2188
+ webView.isInspectable = isInspectable
2189
2189
  }
2190
2190
 
2191
2191
  // First add the webView to view hierarchy
@@ -2341,7 +2341,7 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
2341
2341
  self.view.backgroundColor = parent.view.backgroundColor
2342
2342
  self.title = parent.title ?? request.url?.host ?? "Popup Window"
2343
2343
  self.navigationItem.title = self.title
2344
- self.initWebview()
2344
+ self.initWebview(isInspectable: parent.isInspectable)
2345
2345
  return self.capableWebView
2346
2346
  }
2347
2347
 
@@ -3338,6 +3338,19 @@ extension WKWebViewController: WKUIDelegate {
3338
3338
  // This allows websites to access location when opened with openWebView
3339
3339
  decisionHandler(.grant)
3340
3340
  }
3341
+
3342
+ @available(iOS 15.0, *)
3343
+ public func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
3344
+ print("[InAppBrowser] Media capture permission requested for origin: \(origin.host)")
3345
+
3346
+ // Grant media capture permission automatically, matching Capacitor core's
3347
+ // WebViewDelegationHandler. The OS-level camera/microphone permission
3348
+ // (Info.plist usage description + system prompt) still applies — this only
3349
+ // suppresses WebKit's extra per-origin prompt, which is not persisted
3350
+ // across app launches (WebKit bug 220416) and would otherwise re-prompt
3351
+ // users on every cold start.
3352
+ decisionHandler(.grant)
3353
+ }
3341
3354
  }
3342
3355
 
3343
3356
  // MARK: - Host Blocking Utilities
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-inappbrowser",
3
- "version": "8.13.3",
3
+ "version": "8.13.5",
4
4
  "description": "Capacitor plugin in app browser",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",