Skip to content

Commit 1e63c98

Browse files
authored
feat: add runtime safe margin controls (#464)
* feat: add runtime safe margin controls * fix: harden runtime safe-margin APIs Require explicit boolean input for Android runtime safe-margin toggles instead of silently defaulting, preventing hidden client-side bugs. Guard iOS runtime safe-margin constraint updates when webView is detached from self.view to avoid no-common-ancestor Auto Layout crashes. Document that web implementations of runtime safe-margin APIs are no-ops in generated API docs.
1 parent de1f6f6 commit 1e63c98

File tree

7 files changed

+200
-0
lines changed

7 files changed

+200
-0
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,8 @@ window.mobileApp.close();
275275
* [`removeAllListeners()`](#removealllisteners)
276276
* [`reload(...)`](#reload)
277277
* [`updateDimensions(...)`](#updatedimensions)
278+
* [`setEnabledSafeTopMargin(...)`](#setenabledsafetopmargin)
279+
* [`setEnabledSafeBottomMargin(...)`](#setenabledsafebottommargin)
278280
* [`openSecureWindow(...)`](#opensecurewindow)
279281
* [Interfaces](#interfaces)
280282
* [Type Aliases](#type-aliases)
@@ -705,6 +707,40 @@ When `id` is omitted, targets the active webview.
705707
--------------------
706708

707709

710+
### setEnabledSafeTopMargin(...)
711+
712+
```typescript
713+
setEnabledSafeTopMargin(options: { enabled: boolean; id?: string; }) => Promise<void>
714+
```
715+
716+
Sets the enabled safe top margin of the webview at runtime.
717+
When `id` is omitted, targets the active webview.
718+
On Web, this method is a no-op and resolves without changing layout.
719+
720+
| Param | Type |
721+
| ------------- | ----------------------------------------------- |
722+
| **`options`** | <code>{ enabled: boolean; id?: string; }</code> |
723+
724+
--------------------
725+
726+
727+
### setEnabledSafeBottomMargin(...)
728+
729+
```typescript
730+
setEnabledSafeBottomMargin(options: { enabled: boolean; id?: string; }) => Promise<void>
731+
```
732+
733+
Sets the enabled safe bottom margin of the webview at runtime.
734+
When `id` is omitted, targets the active webview.
735+
On Web, this method is a no-op and resolves without changing layout.
736+
737+
| Param | Type |
738+
| ------------- | ----------------------------------------------- |
739+
| **`options`** | <code>{ enabled: boolean; id?: string; }</code> |
740+
741+
--------------------
742+
743+
708744
### openSecureWindow(...)
709745

710746
```typescript

android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1286,6 +1286,56 @@ public void run() {
12861286
);
12871287
}
12881288

1289+
@PluginMethod
1290+
public void setEnabledSafeTopMargin(PluginCall call) {
1291+
Boolean enabledValue = call.getBoolean("enabled");
1292+
if (enabledValue == null) {
1293+
call.reject("'enabled' (boolean) is required");
1294+
return;
1295+
}
1296+
boolean enabled = enabledValue;
1297+
String targetId = resolveTargetId(call);
1298+
WebViewDialog dialog = resolveDialog(targetId);
1299+
if (dialog == null) {
1300+
call.reject("WebView is not initialized");
1301+
return;
1302+
}
1303+
this.getActivity().runOnUiThread(() -> {
1304+
try {
1305+
dialog.setEnabledSafeTopMargin(enabled);
1306+
call.resolve();
1307+
} catch (Exception e) {
1308+
Log.e("InAppBrowser", "Error setting safe top margin: " + e.getMessage());
1309+
call.reject("Failed to set safe top margin: " + e.getMessage());
1310+
}
1311+
});
1312+
}
1313+
1314+
@PluginMethod
1315+
public void setEnabledSafeBottomMargin(PluginCall call) {
1316+
Boolean enabledValue = call.getBoolean("enabled");
1317+
if (enabledValue == null) {
1318+
call.reject("'enabled' (boolean) is required");
1319+
return;
1320+
}
1321+
boolean enabled = enabledValue;
1322+
String targetId = resolveTargetId(call);
1323+
WebViewDialog dialog = resolveDialog(targetId);
1324+
if (dialog == null) {
1325+
call.reject("WebView is not initialized");
1326+
return;
1327+
}
1328+
this.getActivity().runOnUiThread(() -> {
1329+
try {
1330+
dialog.setEnabledSafeBottomMargin(enabled);
1331+
call.resolve();
1332+
} catch (Exception e) {
1333+
Log.e("InAppBrowser", "Error setting safe bottom margin: " + e.getMessage());
1334+
call.reject("Failed to set safe bottom margin: " + e.getMessage());
1335+
}
1336+
});
1337+
}
1338+
12891339
@PluginMethod
12901340
public void openSecureWindow(PluginCall call) {
12911341
String authEndpoint = call.getString("authEndpoint");

android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3323,6 +3323,22 @@ public void updateDimensions(Integer width, Integer height, Integer x, Integer y
33233323
applyDimensions();
33243324
}
33253325

3326+
public void setEnabledSafeTopMargin(boolean enabled) {
3327+
if (_options.getEnabledSafeTopMargin() == enabled) return;
3328+
_options.setEnabledSafeTopMargin(enabled);
3329+
if (_webView != null) {
3330+
ViewCompat.requestApplyInsets(_webView);
3331+
}
3332+
}
3333+
3334+
public void setEnabledSafeBottomMargin(boolean enabled) {
3335+
if (_options.getEnabledSafeMargin() == enabled) return;
3336+
_options.setEnabledSafeMargin(enabled);
3337+
if (_webView != null) {
3338+
ViewCompat.requestApplyInsets(_webView);
3339+
}
3340+
}
3341+
33263342
/**
33273343
* Convert density-independent pixels (dp) to actual pixels
33283344
*/

ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ public class InAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
4848
CAPPluginMethod(name: "executeScript", returnType: CAPPluginReturnPromise),
4949
CAPPluginMethod(name: "postMessage", returnType: CAPPluginReturnPromise),
5050
CAPPluginMethod(name: "updateDimensions", returnType: CAPPluginReturnPromise),
51+
CAPPluginMethod(name: "setEnabledSafeTopMargin", returnType: CAPPluginReturnPromise),
52+
CAPPluginMethod(name: "setEnabledSafeBottomMargin", returnType: CAPPluginReturnPromise),
5153
CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise),
5254
CAPPluginMethod(name: "openSecureWindow", returnType: CAPPluginReturnPromise),
5355
]
@@ -1368,6 +1370,38 @@ public class InAppBrowserPlugin: CAPPlugin, CAPBridgedPlugin {
13681370
}
13691371
}
13701372

1373+
@objc func setEnabledSafeTopMargin(_ call: CAPPluginCall) {
1374+
if call.options["enabled"] == nil {
1375+
print("[InAppBrowser][Warning] setEnabledSafeTopMargin called without 'enabled'; defaulting to true")
1376+
}
1377+
let enabled = call.getBool("enabled", true)
1378+
DispatchQueue.main.async {
1379+
let targetId = call.getString("id") ?? self.activeWebViewId
1380+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
1381+
call.reject("WebView is not initialized")
1382+
return
1383+
}
1384+
webViewController.updateSafeTopMargin(enabled)
1385+
call.resolve()
1386+
}
1387+
}
1388+
1389+
@objc func setEnabledSafeBottomMargin(_ call: CAPPluginCall) {
1390+
if call.options["enabled"] == nil {
1391+
print("[InAppBrowser][Warning] setEnabledSafeBottomMargin called without 'enabled'; defaulting to false")
1392+
}
1393+
let enabled = call.getBool("enabled", false)
1394+
DispatchQueue.main.async {
1395+
let targetId = call.getString("id") ?? self.activeWebViewId
1396+
guard let webViewController = self.resolveWebViewController(for: targetId) else {
1397+
call.reject("WebView is not initialized")
1398+
return
1399+
}
1400+
webViewController.updateSafeBottomMargin(enabled)
1401+
call.resolve()
1402+
}
1403+
}
1404+
13711405
@objc func openSecureWindow(_ call: CAPPluginCall) {
13721406
guard let urlString = call.getString("authEndpoint") else {
13731407
call.reject("authEndpoint is required")

ios/Sources/InAppBrowserPlugin/WKWebViewController.swift

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,46 @@ extension WKWebViewController: WKNavigationDelegate {
20462046
// Apply the new dimensions
20472047
applyCustomDimensions()
20482048
}
2049+
2050+
open func updateSafeTopMargin(_ enabled: Bool) {
2051+
guard enabled != self.enabledSafeTopMargin else { return }
2052+
self.enabledSafeTopMargin = enabled
2053+
guard let webView = self.webView else { return }
2054+
guard webView.superview === self.view else { return }
2055+
2056+
// Find and deactivate the existing top constraint
2057+
let existingTopConstraints = self.view.constraints.filter {
2058+
($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .top
2059+
}
2060+
NSLayoutConstraint.deactivate(existingTopConstraints)
2061+
2062+
// Create new top constraint based on enabled value
2063+
let topAnchor = enabled ? self.view.safeAreaLayoutGuide.topAnchor : self.view.topAnchor
2064+
NSLayoutConstraint.activate([
2065+
webView.topAnchor.constraint(equalTo: topAnchor)
2066+
])
2067+
self.view.layoutIfNeeded()
2068+
}
2069+
2070+
open func updateSafeBottomMargin(_ enabled: Bool) {
2071+
guard enabled != self.enabledSafeBottomMargin else { return }
2072+
self.enabledSafeBottomMargin = enabled
2073+
guard let webView = self.webView else { return }
2074+
guard webView.superview === self.view else { return }
2075+
2076+
// Find and deactivate the existing bottom constraint
2077+
let existingBottomConstraints = self.view.constraints.filter {
2078+
($0.firstItem as? WKWebView) == webView && $0.firstAttribute == .bottom
2079+
}
2080+
NSLayoutConstraint.deactivate(existingBottomConstraints)
2081+
2082+
// Create new bottom constraint based on enabled value
2083+
let bottomAnchor = enabled ? self.view.safeAreaLayoutGuide.bottomAnchor : self.view.bottomAnchor
2084+
NSLayoutConstraint.activate([
2085+
webView.bottomAnchor.constraint(equalTo: bottomAnchor)
2086+
])
2087+
self.view.layoutIfNeeded()
2088+
}
20492089
}
20502090

20512091
class BlockBarButtonItem: UIBarButtonItem {

src/definitions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,20 @@ export interface InAppBrowserPlugin {
946946
*/
947947
updateDimensions(options: DimensionOptions & { id?: string }): Promise<void>;
948948

949+
/**
950+
* Sets the enabled safe top margin of the webview at runtime.
951+
* When `id` is omitted, targets the active webview.
952+
* On Web, this method is a no-op and resolves without changing layout.
953+
*/
954+
setEnabledSafeTopMargin(options: { enabled: boolean; id?: string }): Promise<void>;
955+
956+
/**
957+
* Sets the enabled safe bottom margin of the webview at runtime.
958+
* When `id` is omitted, targets the active webview.
959+
* On Web, this method is a no-op and resolves without changing layout.
960+
*/
961+
setEnabledSafeBottomMargin(options: { enabled: boolean; id?: string }): Promise<void>;
962+
949963
/**
950964
* Opens a secured window for OAuth2 authentication.
951965
* For web, you should have the code in the redirected page to use a broadcast channel to send the redirected url to the app

src/web.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ export class InAppBrowserWeb extends WebPlugin implements InAppBrowserPlugin {
8989
return;
9090
}
9191

92+
async setEnabledSafeTopMargin(_options: { enabled: boolean; id?: string }): Promise<void> {
93+
console.log('setEnabledSafeTopMargin not supported on web');
94+
return;
95+
}
96+
97+
async setEnabledSafeBottomMargin(_options: { enabled: boolean; id?: string }): Promise<void> {
98+
console.log('setEnabledSafeBottomMargin not supported on web');
99+
return;
100+
}
101+
92102
async openSecureWindow(options: OpenSecureWindowOptions): Promise<OpenSecureWindowResponse> {
93103
const w = 600;
94104
const h = 550;

0 commit comments

Comments
 (0)