From 63c874c25367d20d68d0d1cc44427ba49b1af79b Mon Sep 17 00:00:00 2001 From: "Github Workflow (on behalf of OS-ruimoreiramendes)" Date: Fri, 15 May 2026 16:11:51 +0000 Subject: [PATCH 01/21] chore(release): publish [skip ci] - @capacitor/push-notifications@8.1.1 --- push-notifications/CHANGELOG.md | 4 ++++ push-notifications/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/push-notifications/CHANGELOG.md b/push-notifications/CHANGELOG.md index 307b91207..59002744f 100644 --- a/push-notifications/CHANGELOG.md +++ b/push-notifications/CHANGELOG.md @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## [8.1.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.1.0...@capacitor/push-notifications@8.1.1) (2026-05-15) + +**Note:** Version bump only for package @capacitor/push-notifications + # [8.1.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.4...@capacitor/push-notifications@8.1.0) (2026-05-15) ### Features diff --git a/push-notifications/package.json b/push-notifications/package.json index c7fe01785..0855573a7 100644 --- a/push-notifications/package.json +++ b/push-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/push-notifications", - "version": "8.1.0", + "version": "8.1.1", "description": "The Push Notifications API provides access to native push notifications.", "main": "dist/plugin.cjs.js", "module": "dist/esm/index.js", From 265ca0b1b5294121c35f3e6a3407a2570ecea8fb Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Wed, 20 May 2026 16:38:25 +0200 Subject: [PATCH 02/21] chore: format java code (#2535) --- .../capacitorjs/plugins/app/AppPlugin.java | 24 ++++---- .../LocalNotificationsPlugin.java | 2 +- .../PushNotificationsPlugin.java | 2 +- .../plugins/splashscreen/SplashScreen.java | 59 ++++++++----------- 4 files changed, 37 insertions(+), 50 deletions(-) diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index f14286f26..51c5d01b7 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -33,20 +33,16 @@ public class AppPlugin extends Plugin { public void load() { boolean disableBackButtonHandler = getConfig().getBoolean("disableBackButtonHandler", false); - bridge - .getApp() - .setStatusChangeListener((isActive) -> { - Logger.debug(getLogTag(), "Firing change: " + isActive); - JSObject data = new JSObject(); - data.put("isActive", isActive); - notifyListeners(EVENT_STATE_CHANGE, data, false); - }); - bridge - .getApp() - .setAppRestoredListener((result) -> { - Logger.debug(getLogTag(), "Firing restored result"); - notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); - }); + bridge.getApp().setStatusChangeListener((isActive) -> { + Logger.debug(getLogTag(), "Firing change: " + isActive); + JSObject data = new JSObject(); + data.put("isActive", isActive); + notifyListeners(EVENT_STATE_CHANGE, data, false); + }); + bridge.getApp().setAppRestoredListener((result) -> { + Logger.debug(getLogTag(), "Firing restored result"); + notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); + }); this.onBackPressedCallback = new OnBackPressedCallback(!disableBackButtonHandler) { @Override public void handleOnBackPressed() { diff --git a/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java b/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java index 197de4ae6..fa5352ee8 100644 --- a/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java +++ b/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java @@ -32,7 +32,7 @@ @CapacitorPlugin( name = "LocalNotifications", - permissions = @Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = LocalNotificationsPlugin.LOCAL_NOTIFICATIONS) + permissions = @Permission(strings = {Manifest.permission.POST_NOTIFICATIONS}, alias = LocalNotificationsPlugin.LOCAL_NOTIFICATIONS) ) public class LocalNotificationsPlugin extends Plugin { diff --git a/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java b/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java index 6d339d6df..3c652cf4a 100644 --- a/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java +++ b/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java @@ -26,7 +26,7 @@ @CapacitorPlugin( name = "PushNotifications", - permissions = @Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = PushNotificationsPlugin.PUSH_NOTIFICATIONS) + permissions = @Permission(strings = {Manifest.permission.POST_NOTIFICATIONS}, alias = PushNotificationsPlugin.PUSH_NOTIFICATIONS) ) public class PushNotificationsPlugin extends Plugin { diff --git a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java index af87b3ecf..30aa8f3ae 100644 --- a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java +++ b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java @@ -138,17 +138,14 @@ public boolean onPreDraw() { if (!isVisible && !isHiding) { isVisible = true; - new Handler(context.getMainLooper()).postDelayed( - () -> { - // Splash screen is done... start drawing content. - if (settings.isAutoHide()) { - isVisible = false; - onPreDrawListener = null; - content.getViewTreeObserver().removeOnPreDrawListener(this); - } - }, - settings.getShowDuration() - ); + new Handler(context.getMainLooper()).postDelayed(() -> { + // Splash screen is done... start drawing content. + if (settings.isAutoHide()) { + isVisible = false; + onPreDrawListener = null; + content.getViewTreeObserver().removeOnPreDrawListener(this); + } + }, settings.getShowDuration()); } // Not ready to dismiss splash screen @@ -225,16 +222,13 @@ private void showDialog( isVisible = true; if (settings.isAutoHide()) { - new Handler(context.getMainLooper()).postDelayed( - () -> { - hideDialog(activity, isLaunchSplash); + new Handler(context.getMainLooper()).postDelayed(() -> { + hideDialog(activity, isLaunchSplash); - if (splashListener != null) { - splashListener.completed(); - } - }, - settings.getShowDuration() - ); + if (splashListener != null) { + splashListener.completed(); + } + }, settings.getShowDuration()); } else { // If no autoHide, call complete if (splashListener != null) { @@ -344,12 +338,12 @@ private void buildViews() { Integer spinnerBarColor = config.getSpinnerColor(); if (spinnerBarColor != null) { int[][] states = new int[][] { - new int[] { android.R.attr.state_enabled }, // enabled - new int[] { -android.R.attr.state_enabled }, // disabled - new int[] { -android.R.attr.state_checked }, // unchecked - new int[] { android.R.attr.state_pressed } // pressed + new int[] {android.R.attr.state_enabled}, // enabled + new int[] {-android.R.attr.state_enabled}, // disabled + new int[] {-android.R.attr.state_checked}, // unchecked + new int[] {android.R.attr.state_pressed} // pressed }; - int[] colors = new int[] { spinnerBarColor, spinnerBarColor, spinnerBarColor, spinnerBarColor }; + int[] colors = new int[] {spinnerBarColor, spinnerBarColor, spinnerBarColor, spinnerBarColor}; ColorStateList colorStateList = new ColorStateList(states, colors); spinnerBar.setIndeterminateTintList(colorStateList); } @@ -397,16 +391,13 @@ public void onAnimationEnd(Animator animator) { isVisible = true; if (settings.isAutoHide()) { - new Handler(context.getMainLooper()).postDelayed( - () -> { - hide(settings.getFadeOutDuration(), isLaunchSplash); + new Handler(context.getMainLooper()).postDelayed(() -> { + hide(settings.getFadeOutDuration(), isLaunchSplash); - if (splashListener != null) { - splashListener.completed(); - } - }, - settings.getShowDuration() - ); + if (splashListener != null) { + splashListener.completed(); + } + }, settings.getShowDuration()); } else { // If no autoHide, call complete if (splashListener != null) { From d32b1c6608e1caead377783b05503256e1ee5509 Mon Sep 17 00:00:00 2001 From: jcesarmobile Date: Thu, 21 May 2026 19:27:16 +0200 Subject: [PATCH 03/21] chore: format java code (#2536) --- .../localnotifications/LocalNotificationsPlugin.java | 2 +- .../pushnotifications/PushNotificationsPlugin.java | 2 +- .../capacitorjs/plugins/splashscreen/SplashScreen.java | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java b/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java index fa5352ee8..197de4ae6 100644 --- a/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java +++ b/local-notifications/android/src/main/java/com/capacitorjs/plugins/localnotifications/LocalNotificationsPlugin.java @@ -32,7 +32,7 @@ @CapacitorPlugin( name = "LocalNotifications", - permissions = @Permission(strings = {Manifest.permission.POST_NOTIFICATIONS}, alias = LocalNotificationsPlugin.LOCAL_NOTIFICATIONS) + permissions = @Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = LocalNotificationsPlugin.LOCAL_NOTIFICATIONS) ) public class LocalNotificationsPlugin extends Plugin { diff --git a/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java b/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java index 3c652cf4a..6d339d6df 100644 --- a/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java +++ b/push-notifications/android/src/main/java/com/capacitorjs/plugins/pushnotifications/PushNotificationsPlugin.java @@ -26,7 +26,7 @@ @CapacitorPlugin( name = "PushNotifications", - permissions = @Permission(strings = {Manifest.permission.POST_NOTIFICATIONS}, alias = PushNotificationsPlugin.PUSH_NOTIFICATIONS) + permissions = @Permission(strings = { Manifest.permission.POST_NOTIFICATIONS }, alias = PushNotificationsPlugin.PUSH_NOTIFICATIONS) ) public class PushNotificationsPlugin extends Plugin { diff --git a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java index 30aa8f3ae..756a2d6d5 100644 --- a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java +++ b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java @@ -338,12 +338,12 @@ private void buildViews() { Integer spinnerBarColor = config.getSpinnerColor(); if (spinnerBarColor != null) { int[][] states = new int[][] { - new int[] {android.R.attr.state_enabled}, // enabled - new int[] {-android.R.attr.state_enabled}, // disabled - new int[] {-android.R.attr.state_checked}, // unchecked - new int[] {android.R.attr.state_pressed} // pressed + new int[] { android.R.attr.state_enabled }, // enabled + new int[] { -android.R.attr.state_enabled }, // disabled + new int[] { -android.R.attr.state_checked }, // unchecked + new int[] { android.R.attr.state_pressed } // pressed }; - int[] colors = new int[] {spinnerBarColor, spinnerBarColor, spinnerBarColor, spinnerBarColor}; + int[] colors = new int[] { spinnerBarColor, spinnerBarColor, spinnerBarColor, spinnerBarColor }; ColorStateList colorStateList = new ColorStateList(states, colors); spinnerBar.setIndeterminateTintList(colorStateList); } From 863e98940f95e6d3c8420710c14c2231819fc31a Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Thu, 4 Jun 2026 12:00:53 -0500 Subject: [PATCH 04/21] feat(app): add backGesture event for Android predictive back --- app/README.md | 33 +++++++++ .../capacitorjs/plugins/app/AppPlugin.java | 73 +++++++++++++++++++ app/src/definitions.ts | 11 +++ 3 files changed, 117 insertions(+) diff --git a/app/README.md b/app/README.md index d7ef3bb4f..6913244b6 100644 --- a/app/README.md +++ b/app/README.md @@ -127,6 +127,7 @@ export default config; * [`addListener('appUrlOpen', ...)`](#addlistenerappurlopen-) * [`addListener('appRestoredResult', ...)`](#addlistenerapprestoredresult-) * [`addListener('backButton', ...)`](#addlistenerbackbutton-) +* [`addListener('backGesture', ...)`](#addlistenerbackgesture-) * [`removeAllListeners()`](#removealllisteners) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) @@ -403,6 +404,22 @@ If you want to close the app, call `App.exitApp()`. -------------------- +### addListener('backGesture', ...) + +```typescript +addListener(eventName: 'backGesture', listenerFunc: BackGestureListener) => Promise +``` + +| Param | Type | +| ------------------ | ------------------------------------------------------------------- | +| **`eventName`** | 'backGesture' | +| **`listenerFunc`** | BackGestureListener | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + ### removeAllListeners() ```typescript @@ -491,6 +508,17 @@ Remove all native listeners for this plugin | **`canGoBack`** | boolean | Indicates whether the browser can go back in history. False when the history stack is on the first entry. | 1.0.0 | +#### BackGestureListenerEvent + +| Prop | Type | +| --------------- | ---------------------------------------------------------- | +| **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | +| **`progress`** | number | +| **`swipeEdge`** | 'left' \| 'right' | +| **`touchX`** | number | +| **`touchY`** | number | + + ### Type Aliases @@ -513,4 +541,9 @@ Remove all native listeners for this plugin (event: BackButtonListenerEvent): void + +#### BackGestureListener + +(event: BackGestureListenerEvent): void + diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index 51c5d01b7..db2d809d0 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -1,10 +1,20 @@ package com.capacitorjs.plugins.app; +import static android.window.BackEvent.EDGE_LEFT; +import static android.window.BackEvent.EDGE_NONE; +import static android.window.BackEvent.EDGE_RIGHT; + import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.net.Uri; +import android.os.Build; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; + +import androidx.activity.BackEventCompat; import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.pm.PackageInfoCompat; import androidx.core.os.LocaleListCompat; @@ -21,6 +31,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_BACK_BUTTON = "backButton"; + private static final String EVENT_BACK_GESTURE = "backGesture"; private static final String EVENT_URL_OPEN = "appUrlOpen"; private static final String EVENT_STATE_CHANGE = "appStateChange"; private static final String EVENT_RESTORED_RESULT = "appRestoredResult"; @@ -29,6 +40,7 @@ public class AppPlugin extends Plugin { private boolean hasPausedEver = false; private OnBackPressedCallback onBackPressedCallback; + private OnBackAnimationCallback onBackAnimationCallback; public void load() { boolean disableBackButtonHandler = getConfig().getBoolean("disableBackButtonHandler", false); @@ -59,6 +71,58 @@ public void handleOnBackPressed() { } }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + this.onBackAnimationCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + if (hasListeners(EVENT_BACK_GESTURE)) { + JSObject data = new JSObject(); + data.put("phase", "commit"); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + } + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "start"); + data.put("progress", backEvent.getProgress()); + data.put("swipeEdge", backEvent.getSwipeEdge()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "progress"); + data.put("progress", backEvent.getProgress()); + data.put("swipeEdge", backEvent.getSwipeEdge()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + + @Override + public void onBackCancelled() { + OnBackAnimationCallback.super.onBackCancelled(); + + JSObject data = new JSObject(); + data.put("phase", "cancel"); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + }; + } + getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); } @@ -178,4 +242,13 @@ private void unsetAppListeners() { bridge.getApp().setStatusChangeListener(null); bridge.getApp().setAppRestoredListener(null); } + + private String getSwipeEdge(int edge) { + return switch (edge) { + case EDGE_LEFT -> "left"; + case EDGE_RIGHT -> "right"; + case EDGE_NONE -> "none"; + default -> "none"; + }; + } } diff --git a/app/src/definitions.ts b/app/src/definitions.ts index 1029be2ec..ef6ae6d58 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -144,6 +144,14 @@ export interface BackButtonListenerEvent { canGoBack: boolean; } +export interface BackGestureListenerEvent { + phase: 'start' | 'progress' | 'cancel' | 'commit'; + progress?: number; + swipeEdge?: 'left' | 'right' | 'none'; + touchX?: number; + touchY?: number; +} + export interface ToggleBackButtonHandlerOptions { /** * Indicates whether to enable or disable default back button handling. @@ -157,6 +165,7 @@ export type StateChangeListener = (state: AppState) => void; export type URLOpenListener = (event: URLOpenListenerEvent) => void; export type RestoredListener = (event: RestoredListenerEvent) => void; export type BackButtonListener = (event: BackButtonListenerEvent) => void; +export type BackGestureListener = (event: BackGestureListenerEvent) => void; export interface AppLanguageCode { /** @@ -303,6 +312,8 @@ export interface AppPlugin { */ addListener(eventName: 'backButton', listenerFunc: BackButtonListener): Promise; + addListener(eventName: 'backGesture', listenerFunc: BackGestureListener): Promise; + /** * Remove all native listeners for this plugin * From 2aa58ada3e8dd75d62fdd824d6132b94481954ce Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Fri, 5 Jun 2026 11:09:22 -0500 Subject: [PATCH 05/21] feat(app): add backGesture event for iOS back / forward edge gestures --- app/ios/Sources/AppPlugin/AppPlugin.swift | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index c93b84a6d..45c7de55d 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -17,6 +17,8 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { private var observers: [NSObjectProtocol] = [] override public func load() { + loadGestureRecognizers() + NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.handleUniversalLink(notification:)), name: Notification.Name.capacitorOpenUniversalLink, object: nil) observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (_) in @@ -125,4 +127,86 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } + + @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { + guard let view = bridge?.webView else { return } + + let translation = recognizer.translation(in: webView) + let viewWidth = view.bounds.width + let viewHeight = view.bounds.height + + var data: [String: Any] = [:] + + switch recognizer.edges { + case .left: + let progress = translation.x / viewWidth + data["swipeEdge"] = "left" + data["progress"] = max(0, min(1, progress)) + + case .right: + let progress = -translation.x / viewWidth + data["swipeEdge"] = "right" + data["progress"] = max(0, min(1, progress)) + case .top: + let progress = translation.y / viewHeight + data["swipeEdge"] = "top" + data["progress"] = max(0, min(1, progress)) + case .bottom: + let progress = -translation.y / viewHeight + data["swipeEdge"] = "bottom" + data["progress"] = max(0, min(1, progress)) + case .all: + break + default: + break + } + + switch recognizer.state { + case .began: + data["phase"] = "start" + case .changed: + data["phase"] = "progress" + case .ended: + data["phase"] = "commit" + case .cancelled: + data["phase"] = "cancel" + case .failed: + data["phase"] = "cancel" + case .possible: + break + @unknown default: + break + } + + notifyListeners("backGesture", data: data) + } + + private func loadGestureRecognizers() { + guard let window = UIApplication.shared.delegate?.window ?? nil else { return } + + let leftEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleEdgePan(_:))) + leftEdgePanRecognizer.delegate = self + leftEdgePanRecognizer.edges = .left + + let rightEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleEdgePan(_:))) + rightEdgePanRecognizer.delegate = self + rightEdgePanRecognizer.edges = .right + + window.addGestureRecognizer(leftEdgePanRecognizer) + window.addGestureRecognizer(rightEdgePanRecognizer) + } +} + +extension AppPlugin: UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } } From e21809c6ce0fe9ff54954a6e49522558a2e46eb5 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Fri, 5 Jun 2026 12:32:02 -0500 Subject: [PATCH 06/21] feat(app): add runtime toggle for back gesture handler --- app/README.md | 21 +- .../capacitorjs/plugins/app/AppPlugin.java | 213 ++++++++++++------ app/ios/Sources/AppPlugin/AppPlugin.swift | 34 ++- app/src/definitions.ts | 4 + app/src/web.ts | 4 + 5 files changed, 205 insertions(+), 71 deletions(-) diff --git a/app/README.md b/app/README.md index 6913244b6..45111d74d 100644 --- a/app/README.md +++ b/app/README.md @@ -75,6 +75,7 @@ const checkAppLaunchUrl = async () => { | Prop | Type | Description | Default | Since | | ------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ------------------ | ----- | | **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | +| **`backGestureEnabled`** | boolean | | | | ### Examples @@ -84,7 +85,8 @@ In `capacitor.config.json`: { "plugins": { "App": { - "disableBackButtonHandler": true + "disableBackButtonHandler": true, + "backGestureEnabled": undefined } } } @@ -101,6 +103,7 @@ const config: CapacitorConfig = { plugins: { App: { disableBackButtonHandler: true, + backGestureEnabled: undefined, }, }, }; @@ -121,6 +124,7 @@ export default config; * [`minimizeApp()`](#minimizeapp) * [`getAppLanguage()`](#getapplanguage) * [`toggleBackButtonHandler(...)`](#togglebackbuttonhandler) +* [`setBackGestureEnabled(...)`](#setbackgestureenabled) * [`addListener('appStateChange', ...)`](#addlistenerappstatechange-) * [`addListener('pause', ...)`](#addlistenerpause-) * [`addListener('resume', ...)`](#addlistenerresume-) @@ -247,6 +251,19 @@ Only available for Android. -------------------- +### setBackGestureEnabled(...) + +```typescript +setBackGestureEnabled(options: { enabled: boolean; }) => Promise +``` + +| Param | Type | +| ------------- | ---------------------------------- | +| **`options`** | { enabled: boolean; } | + +-------------------- + + ### addListener('appStateChange', ...) ```typescript @@ -514,7 +531,7 @@ Remove all native listeners for this plugin | --------------- | ---------------------------------------------------------- | | **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | | **`progress`** | number | -| **`swipeEdge`** | 'left' \| 'right' | +| **`swipeEdge`** | 'none' \| 'left' \| 'right' | | **`touchX`** | number | | **`touchY`** | number | diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index db2d809d0..7ee3dbd03 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -11,8 +11,8 @@ import android.os.Build; import android.window.BackEvent; import android.window.OnBackAnimationCallback; +import android.window.OnBackInvokedDispatcher; -import androidx.activity.BackEventCompat; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; @@ -38,24 +38,38 @@ public class AppPlugin extends Plugin { private static final String EVENT_PAUSE = "pause"; private static final String EVENT_RESUME = "resume"; private boolean hasPausedEver = false; + private boolean backButtonHandlerEnabled = false; + private boolean backGestureHandlerEnabled = false; private OnBackPressedCallback onBackPressedCallback; private OnBackAnimationCallback onBackAnimationCallback; + private String activeEdge = null; + private Float lastEdgeProgress = null; + private Float lastEdgeTouchX = null; + private Float lastEdgeTouchY = null; + + public void load() { - boolean disableBackButtonHandler = getConfig().getBoolean("disableBackButtonHandler", false); - - bridge.getApp().setStatusChangeListener((isActive) -> { - Logger.debug(getLogTag(), "Firing change: " + isActive); - JSObject data = new JSObject(); - data.put("isActive", isActive); - notifyListeners(EVENT_STATE_CHANGE, data, false); - }); - bridge.getApp().setAppRestoredListener((result) -> { - Logger.debug(getLogTag(), "Firing restored result"); - notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); - }); - this.onBackPressedCallback = new OnBackPressedCallback(!disableBackButtonHandler) { + this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); + this.backGestureHandlerEnabled = getConfig().getBoolean("enableBackGestureHandler", false); + + bridge + .getApp() + .setStatusChangeListener((isActive) -> { + Logger.debug(getLogTag(), "Firing change: " + isActive); + JSObject data = new JSObject(); + data.put("isActive", isActive); + notifyListeners(EVENT_STATE_CHANGE, data, false); + }); + bridge + .getApp() + .setAppRestoredListener((result) -> { + Logger.debug(getLogTag(), "Firing restored result"); + notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); + }); + + this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !backGestureHandlerEnabled) { @Override public void handleOnBackPressed() { if (!hasListeners(EVENT_BACK_BUTTON)) { @@ -71,56 +85,8 @@ public void handleOnBackPressed() { } }; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - this.onBackAnimationCallback = new OnBackAnimationCallback() { - @Override - public void onBackInvoked() { - if (hasListeners(EVENT_BACK_GESTURE)) { - JSObject data = new JSObject(); - data.put("phase", "commit"); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - } - - @Override - public void onBackStarted(@NonNull BackEvent backEvent) { - OnBackAnimationCallback.super.onBackStarted(backEvent); - JSObject data = new JSObject(); - data.put("phase", "start"); - data.put("progress", backEvent.getProgress()); - data.put("swipeEdge", backEvent.getSwipeEdge()); - data.put("touchX", backEvent.getTouchX()); - data.put("touchY", backEvent.getTouchY()); - data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - - @Override - public void onBackProgressed(@NonNull BackEvent backEvent) { - OnBackAnimationCallback.super.onBackStarted(backEvent); - JSObject data = new JSObject(); - data.put("phase", "progress"); - data.put("progress", backEvent.getProgress()); - data.put("swipeEdge", backEvent.getSwipeEdge()); - data.put("touchX", backEvent.getTouchX()); - data.put("touchY", backEvent.getTouchY()); - data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - - @Override - public void onBackCancelled() { - OnBackAnimationCallback.super.onBackCancelled(); - - JSObject data = new JSObject(); - data.put("phase", "cancel"); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - }; + if (this.backGestureHandlerEnabled) { + this.setupBackGestureHandlers(); } getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); @@ -183,9 +149,10 @@ public void toggleBackButtonHandler(PluginCall call) { return; } - Boolean enabled = call.getBoolean("enabled"); + Boolean enabled = call.getBoolean("enabled", false); + backButtonHandlerEnabled = enabled; - this.onBackPressedCallback.setEnabled(enabled); + this.onBackPressedCallback.setEnabled(backButtonHandlerEnabled); call.resolve(); } @@ -198,6 +165,22 @@ public void getAppLanguage(PluginCall call) { call.resolve(ret); } + @PluginMethod + public void setBackGestureEnabled(PluginCall call) { + Boolean enabled = call.getBoolean("enabled", false); + backGestureHandlerEnabled = enabled; + + if (backGestureHandlerEnabled) { + this.onBackPressedCallback.setEnabled(false); + setupBackGestureHandlers(); + } else { + this.onBackPressedCallback.setEnabled(this.backButtonHandlerEnabled); + teardownBackGestureHandlers(); + } + + call.resolve(); + } + /** * Handle ACTION_VIEW intents to store a URL that was used to open the app * @param intent @@ -243,6 +226,104 @@ private void unsetAppListeners() { bridge.getApp().setAppRestoredListener(null); } + private void setupBackGestureHandlers() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + this.onBackAnimationCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + if (hasListeners(EVENT_BACK_GESTURE)) { + JSObject data = new JSObject(); + data.put("phase", "commit"); + data.put("progress", lastEdgeProgress); + data.put("touchX", lastEdgeTouchX); + data.put("touchY", lastEdgeTouchY); + data.put("swipeEdge", activeEdge); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = null; + lastEdgeTouchX = null; + lastEdgeTouchY = null; + activeEdge = null; + } + } + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "start"); + data.put("progress", backEvent.getProgress()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = backEvent.getProgress(); + lastEdgeTouchX = backEvent.getTouchX(); + lastEdgeTouchY = backEvent.getTouchY(); + activeEdge = getSwipeEdge(backEvent.getSwipeEdge()); + } + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "progress"); + data.put("progress", backEvent.getProgress()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = backEvent.getProgress(); + lastEdgeTouchX = backEvent.getTouchX(); + lastEdgeTouchY = backEvent.getTouchY(); + activeEdge = getSwipeEdge(backEvent.getSwipeEdge()); + } + } + + @Override + public void onBackCancelled() { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackCancelled(); + + JSObject data = new JSObject(); + data.put("phase", "cancel"); + data.put("progress", lastEdgeProgress); + data.put("touchX", lastEdgeTouchX); + data.put("touchY", lastEdgeTouchY); + data.put("swipeEdge", activeEdge); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = null; + lastEdgeTouchX = null; + lastEdgeTouchY = null; + activeEdge = null; + } + } + }; + + getActivity().getOnBackInvokedDispatcher().registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + this.onBackAnimationCallback + ); + } + } + + private void teardownBackGestureHandlers() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + getActivity().getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(this.onBackAnimationCallback); + this.onBackAnimationCallback = null; + } + } + private String getSwipeEdge(int edge) { return switch (edge) { case EDGE_LEFT -> "left"; diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 45c7de55d..df7738386 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -12,12 +12,17 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "getLaunchUrl", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "minimizeApp", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setBackGestureEnabled", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] - + private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil + private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil + override public func load() { - loadGestureRecognizers() + if getConfig().getBoolean("enableBackGestureHandler", false) { + loadGestureRecognizers() + } NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.handleUniversalLink(notification:)), name: Notification.Name.capacitorOpenUniversalLink, object: nil) @@ -127,6 +132,16 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } + + @objc func setBackGestureEnabled(_ call: CAPPluginCall) { + if (call.getBool("enabled", false)) { + loadGestureRecognizers() + } else { + destroyGestureRecognizers() + } + + call.resolve() + } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { guard let view = bridge?.webView else { return } @@ -194,6 +209,19 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { window.addGestureRecognizer(leftEdgePanRecognizer) window.addGestureRecognizer(rightEdgePanRecognizer) + + self.leftEdgePanRecognizer = leftEdgePanRecognizer + self.rightEdgePanRecognizer = rightEdgePanRecognizer + } + + private func destroyGestureRecognizers() { + guard let window = UIApplication.shared.delegate?.window ?? nil, + let leftEdgePanRecognizer = self.leftEdgePanRecognizer, + let rightEdgePanRecognizer = self.rightEdgePanRecognizer + else { return } + + window.removeGestureRecognizer(leftEdgePanRecognizer) + window.removeGestureRecognizer(rightEdgePanRecognizer) } } diff --git a/app/src/definitions.ts b/app/src/definitions.ts index ef6ae6d58..6ba79b287 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -15,6 +15,8 @@ declare module '@capacitor/cli' { * @example true */ disableBackButtonHandler?: boolean; + + backGestureEnabled?: boolean; }; } } @@ -233,6 +235,8 @@ export interface AppPlugin { */ toggleBackButtonHandler(options: ToggleBackButtonHandlerOptions): Promise; + setBackGestureEnabled(options: { enabled: boolean }): Promise; + /** * Listen for changes in the app or the activity states. * diff --git a/app/src/web.ts b/app/src/web.ts index be23cc767..585024538 100644 --- a/app/src/web.ts +++ b/app/src/web.ts @@ -32,6 +32,10 @@ export class AppWeb extends WebPlugin implements AppPlugin { throw this.unimplemented('Not implemented on web.'); } + async setBackGestureEnabled(): Promise { + throw new Error('Method not implemented.'); + } + private handleVisibilityChange = () => { const data = { isActive: document.hidden !== true, From 134b0e249bc41b4dc30c896b8b0ade28fce93a8f Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:04 -0500 Subject: [PATCH 07/21] refactor(app): rename backGesture API to edgeGesture --- app/README.md | 41 +++++++++++-------- .../capacitorjs/plugins/app/AppPlugin.java | 16 ++++---- app/ios/Sources/AppPlugin/AppPlugin.swift | 22 +++++----- app/src/definitions.ts | 14 ++++--- app/src/web.ts | 2 +- 5 files changed, 54 insertions(+), 41 deletions(-) diff --git a/app/README.md b/app/README.md index 45111d74d..6ad5c8964 100644 --- a/app/README.md +++ b/app/README.md @@ -75,7 +75,7 @@ const checkAppLaunchUrl = async () => { | Prop | Type | Description | Default | Since | | ------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ------------------ | ----- | | **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | -| **`backGestureEnabled`** | boolean | | | | +| **`enableEdgeGestureHandler`** | boolean | | | | ### Examples @@ -86,7 +86,7 @@ In `capacitor.config.json`: "plugins": { "App": { "disableBackButtonHandler": true, - "backGestureEnabled": undefined + "enableEdgeGestureHandler": undefined } } } @@ -103,7 +103,7 @@ const config: CapacitorConfig = { plugins: { App: { disableBackButtonHandler: true, - backGestureEnabled: undefined, + enableEdgeGestureHandler: undefined, }, }, }; @@ -124,14 +124,14 @@ export default config; * [`minimizeApp()`](#minimizeapp) * [`getAppLanguage()`](#getapplanguage) * [`toggleBackButtonHandler(...)`](#togglebackbuttonhandler) -* [`setBackGestureEnabled(...)`](#setbackgestureenabled) +* [`toggleEdgeGestureHandler(...)`](#toggleedgegesturehandler) * [`addListener('appStateChange', ...)`](#addlistenerappstatechange-) * [`addListener('pause', ...)`](#addlistenerpause-) * [`addListener('resume', ...)`](#addlistenerresume-) * [`addListener('appUrlOpen', ...)`](#addlistenerappurlopen-) * [`addListener('appRestoredResult', ...)`](#addlistenerapprestoredresult-) * [`addListener('backButton', ...)`](#addlistenerbackbutton-) -* [`addListener('backGesture', ...)`](#addlistenerbackgesture-) +* [`addListener('edgeGesture', ...)`](#addlisteneredgegesture-) * [`removeAllListeners()`](#removealllisteners) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) @@ -251,15 +251,15 @@ Only available for Android. -------------------- -### setBackGestureEnabled(...) +### toggleEdgeGestureHandler(...) ```typescript -setBackGestureEnabled(options: { enabled: boolean; }) => Promise +toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions) => Promise ``` -| Param | Type | -| ------------- | ---------------------------------- | -| **`options`** | { enabled: boolean; } | +| Param | Type | +| ------------- | ------------------------------------------------------------------------------------------- | +| **`options`** | ToggleEdgeGestureHandlerOptions | -------------------- @@ -421,16 +421,16 @@ If you want to close the app, call `App.exitApp()`. -------------------- -### addListener('backGesture', ...) +### addListener('edgeGesture', ...) ```typescript -addListener(eventName: 'backGesture', listenerFunc: BackGestureListener) => Promise +addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener) => Promise ``` | Param | Type | | ------------------ | ------------------------------------------------------------------- | -| **`eventName`** | 'backGesture' | -| **`listenerFunc`** | BackGestureListener | +| **`eventName`** | 'edgeGesture' | +| **`listenerFunc`** | EdgeGestureListener | **Returns:** Promise<PluginListenerHandle> @@ -491,6 +491,13 @@ Remove all native listeners for this plugin | **`enabled`** | boolean | Indicates whether to enable or disable default back button handling. | 7.1.0 | +#### ToggleEdgeGestureHandlerOptions + +| Prop | Type | +| ------------- | -------------------- | +| **`enabled`** | boolean | + + #### PluginListenerHandle | Prop | Type | @@ -525,7 +532,7 @@ Remove all native listeners for this plugin | **`canGoBack`** | boolean | Indicates whether the browser can go back in history. False when the history stack is on the first entry. | 1.0.0 | -#### BackGestureListenerEvent +#### EdgeGestureListenerEvent | Prop | Type | | --------------- | ---------------------------------------------------------- | @@ -559,8 +566,8 @@ Remove all native listeners for this plugin (event: BackButtonListenerEvent): void -#### BackGestureListener +#### EdgeGestureListener -(event: BackGestureListenerEvent): void +(event: EdgeGestureListenerEvent): void diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index 7ee3dbd03..5c16dd66c 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -31,7 +31,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_BACK_BUTTON = "backButton"; - private static final String EVENT_BACK_GESTURE = "backGesture"; + private static final String EVENT_BACK_GESTURE = "edgeGesture"; private static final String EVENT_URL_OPEN = "appUrlOpen"; private static final String EVENT_STATE_CHANGE = "appStateChange"; private static final String EVENT_RESTORED_RESULT = "appRestoredResult"; @@ -39,7 +39,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_RESUME = "resume"; private boolean hasPausedEver = false; private boolean backButtonHandlerEnabled = false; - private boolean backGestureHandlerEnabled = false; + private boolean edgeGestureHandlerEnabled = false; private OnBackPressedCallback onBackPressedCallback; private OnBackAnimationCallback onBackAnimationCallback; @@ -52,7 +52,7 @@ public class AppPlugin extends Plugin { public void load() { this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); - this.backGestureHandlerEnabled = getConfig().getBoolean("enableBackGestureHandler", false); + this.edgeGestureHandlerEnabled = getConfig().getBoolean("enableEdgeGestureHandler", false); bridge .getApp() @@ -69,7 +69,7 @@ public void load() { notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); }); - this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !backGestureHandlerEnabled) { + this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !edgeGestureHandlerEnabled) { @Override public void handleOnBackPressed() { if (!hasListeners(EVENT_BACK_BUTTON)) { @@ -85,7 +85,7 @@ public void handleOnBackPressed() { } }; - if (this.backGestureHandlerEnabled) { + if (this.edgeGestureHandlerEnabled) { this.setupBackGestureHandlers(); } @@ -166,11 +166,11 @@ public void getAppLanguage(PluginCall call) { } @PluginMethod - public void setBackGestureEnabled(PluginCall call) { + public void toggleEdgeGestureHandler(PluginCall call) { Boolean enabled = call.getBoolean("enabled", false); - backGestureHandlerEnabled = enabled; + edgeGestureHandlerEnabled = enabled; - if (backGestureHandlerEnabled) { + if (edgeGestureHandlerEnabled) { this.onBackPressedCallback.setEnabled(false); setupBackGestureHandlers(); } else { diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index df7738386..98ae34248 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -13,14 +13,14 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "minimizeApp", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "setBackGestureEnabled", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "toggleEdgeGestureHandler", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil override public func load() { - if getConfig().getBoolean("enableBackGestureHandler", false) { + if getConfig().getBoolean("enableEdgeGestureHandler", false) { loadGestureRecognizers() } @@ -133,14 +133,16 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { call.unimplemented() } - @objc func setBackGestureEnabled(_ call: CAPPluginCall) { - if (call.getBool("enabled", false)) { - loadGestureRecognizers() - } else { - destroyGestureRecognizers() + @objc func toggleEdgeGestureHandler(_ call: CAPPluginCall) { + DispatchQueue.main.async { + if (call.getBool("enabled", false)) { + self.loadGestureRecognizers() + } else { + self.destroyGestureRecognizers() + } + + call.resolve() } - - call.resolve() } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { @@ -193,7 +195,7 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { break } - notifyListeners("backGesture", data: data) + notifyListeners("edgeGesture", data: data) } private func loadGestureRecognizers() { diff --git a/app/src/definitions.ts b/app/src/definitions.ts index 6ba79b287..f61ff5660 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -16,7 +16,7 @@ declare module '@capacitor/cli' { */ disableBackButtonHandler?: boolean; - backGestureEnabled?: boolean; + enableEdgeGestureHandler?: boolean; }; } } @@ -146,7 +146,7 @@ export interface BackButtonListenerEvent { canGoBack: boolean; } -export interface BackGestureListenerEvent { +export interface EdgeGestureListenerEvent { phase: 'start' | 'progress' | 'cancel' | 'commit'; progress?: number; swipeEdge?: 'left' | 'right' | 'none'; @@ -163,11 +163,15 @@ export interface ToggleBackButtonHandlerOptions { enabled: boolean; } +export interface ToggleEdgeGestureHandlerOptions { + enabled: boolean; +} + export type StateChangeListener = (state: AppState) => void; export type URLOpenListener = (event: URLOpenListenerEvent) => void; export type RestoredListener = (event: RestoredListenerEvent) => void; export type BackButtonListener = (event: BackButtonListenerEvent) => void; -export type BackGestureListener = (event: BackGestureListenerEvent) => void; +export type EdgeGestureListener = (event: EdgeGestureListenerEvent) => void; export interface AppLanguageCode { /** @@ -235,7 +239,7 @@ export interface AppPlugin { */ toggleBackButtonHandler(options: ToggleBackButtonHandlerOptions): Promise; - setBackGestureEnabled(options: { enabled: boolean }): Promise; + toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions): Promise; /** * Listen for changes in the app or the activity states. @@ -316,7 +320,7 @@ export interface AppPlugin { */ addListener(eventName: 'backButton', listenerFunc: BackButtonListener): Promise; - addListener(eventName: 'backGesture', listenerFunc: BackGestureListener): Promise; + addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener): Promise; /** * Remove all native listeners for this plugin diff --git a/app/src/web.ts b/app/src/web.ts index 585024538..ca68695f9 100644 --- a/app/src/web.ts +++ b/app/src/web.ts @@ -32,7 +32,7 @@ export class AppWeb extends WebPlugin implements AppPlugin { throw this.unimplemented('Not implemented on web.'); } - async setBackGestureEnabled(): Promise { + async toggleEdgeGestureHandler(): Promise { throw new Error('Method not implemented.'); } From 96ab212b505b0aa1d7ef7ae2c0b28c74a4442807 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:26 -0500 Subject: [PATCH 08/21] feat(app): include touch coordinates in iOS edge gesture event --- app/ios/Sources/AppPlugin/AppPlugin.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 98ae34248..8ddc6fca5 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -149,10 +149,14 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { guard let view = bridge?.webView else { return } let translation = recognizer.translation(in: webView) + let touch = recognizer.location(in: view) let viewWidth = view.bounds.width let viewHeight = view.bounds.height var data: [String: Any] = [:] + + data["touchX"] = touch.x + data["touchY"] = touch.y switch recognizer.edges { case .left: From fda8a5df2548ae76ec28f35876b1cfda0aa88abe Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:49 -0500 Subject: [PATCH 09/21] fmt --- .../capacitorjs/plugins/app/AppPlugin.java | 9 ++-- app/ios/Sources/AppPlugin/AppPlugin.swift | 44 +++++++++---------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index 5c16dd66c..028b06612 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -12,7 +12,6 @@ import android.window.BackEvent; import android.window.OnBackAnimationCallback; import android.window.OnBackInvokedDispatcher; - import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; @@ -49,7 +48,6 @@ public class AppPlugin extends Plugin { private Float lastEdgeTouchX = null; private Float lastEdgeTouchY = null; - public void load() { this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); this.edgeGestureHandlerEnabled = getConfig().getBoolean("enableEdgeGestureHandler", false); @@ -310,10 +308,9 @@ public void onBackCancelled() { } }; - getActivity().getOnBackInvokedDispatcher().registerOnBackInvokedCallback( - OnBackInvokedDispatcher.PRIORITY_DEFAULT, - this.onBackAnimationCallback - ); + getActivity() + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this.onBackAnimationCallback); } } diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 8ddc6fca5..4c54987c3 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -16,12 +16,12 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "toggleEdgeGestureHandler", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] - private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil - private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil - + private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? + private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? + override public func load() { if getConfig().getBoolean("enableEdgeGestureHandler", false) { - loadGestureRecognizers() + loadGestureRecognizers() } NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) @@ -132,17 +132,17 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } - + @objc func toggleEdgeGestureHandler(_ call: CAPPluginCall) { - DispatchQueue.main.async { - if (call.getBool("enabled", false)) { - self.loadGestureRecognizers() - } else { - self.destroyGestureRecognizers() + DispatchQueue.main.async { + if call.getBool("enabled", false) { + self.loadGestureRecognizers() + } else { + self.destroyGestureRecognizers() + } + + call.resolve() } - - call.resolve() - } } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { @@ -154,7 +154,7 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { let viewHeight = view.bounds.height var data: [String: Any] = [:] - + data["touchX"] = touch.x data["touchY"] = touch.y @@ -215,19 +215,19 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { window.addGestureRecognizer(leftEdgePanRecognizer) window.addGestureRecognizer(rightEdgePanRecognizer) - + self.leftEdgePanRecognizer = leftEdgePanRecognizer self.rightEdgePanRecognizer = rightEdgePanRecognizer } - + private func destroyGestureRecognizers() { - guard let window = UIApplication.shared.delegate?.window ?? nil, - let leftEdgePanRecognizer = self.leftEdgePanRecognizer, - let rightEdgePanRecognizer = self.rightEdgePanRecognizer + guard let window = UIApplication.shared.delegate?.window ?? nil, + let leftEdgePanRecognizer = self.leftEdgePanRecognizer, + let rightEdgePanRecognizer = self.rightEdgePanRecognizer else { return } - - window.removeGestureRecognizer(leftEdgePanRecognizer) - window.removeGestureRecognizer(rightEdgePanRecognizer) + + window.removeGestureRecognizer(leftEdgePanRecognizer) + window.removeGestureRecognizer(rightEdgePanRecognizer) } } From a3fc324363378287547585302baae10472722253 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Thu, 4 Jun 2026 12:00:53 -0500 Subject: [PATCH 10/21] feat(app): add backGesture event for Android predictive back --- app/README.md | 33 +++++++++ .../capacitorjs/plugins/app/AppPlugin.java | 73 +++++++++++++++++++ app/src/definitions.ts | 11 +++ 3 files changed, 117 insertions(+) diff --git a/app/README.md b/app/README.md index d7ef3bb4f..6913244b6 100644 --- a/app/README.md +++ b/app/README.md @@ -127,6 +127,7 @@ export default config; * [`addListener('appUrlOpen', ...)`](#addlistenerappurlopen-) * [`addListener('appRestoredResult', ...)`](#addlistenerapprestoredresult-) * [`addListener('backButton', ...)`](#addlistenerbackbutton-) +* [`addListener('backGesture', ...)`](#addlistenerbackgesture-) * [`removeAllListeners()`](#removealllisteners) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) @@ -403,6 +404,22 @@ If you want to close the app, call `App.exitApp()`. -------------------- +### addListener('backGesture', ...) + +```typescript +addListener(eventName: 'backGesture', listenerFunc: BackGestureListener) => Promise +``` + +| Param | Type | +| ------------------ | ------------------------------------------------------------------- | +| **`eventName`** | 'backGesture' | +| **`listenerFunc`** | BackGestureListener | + +**Returns:** Promise<PluginListenerHandle> + +-------------------- + + ### removeAllListeners() ```typescript @@ -491,6 +508,17 @@ Remove all native listeners for this plugin | **`canGoBack`** | boolean | Indicates whether the browser can go back in history. False when the history stack is on the first entry. | 1.0.0 | +#### BackGestureListenerEvent + +| Prop | Type | +| --------------- | ---------------------------------------------------------- | +| **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | +| **`progress`** | number | +| **`swipeEdge`** | 'left' \| 'right' | +| **`touchX`** | number | +| **`touchY`** | number | + + ### Type Aliases @@ -513,4 +541,9 @@ Remove all native listeners for this plugin (event: BackButtonListenerEvent): void + +#### BackGestureListener + +(event: BackGestureListenerEvent): void + diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index f14286f26..f97870646 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -1,10 +1,20 @@ package com.capacitorjs.plugins.app; +import static android.window.BackEvent.EDGE_LEFT; +import static android.window.BackEvent.EDGE_NONE; +import static android.window.BackEvent.EDGE_RIGHT; + import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.net.Uri; +import android.os.Build; +import android.window.BackEvent; +import android.window.OnBackAnimationCallback; + +import androidx.activity.BackEventCompat; import androidx.activity.OnBackPressedCallback; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.pm.PackageInfoCompat; import androidx.core.os.LocaleListCompat; @@ -21,6 +31,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_BACK_BUTTON = "backButton"; + private static final String EVENT_BACK_GESTURE = "backGesture"; private static final String EVENT_URL_OPEN = "appUrlOpen"; private static final String EVENT_STATE_CHANGE = "appStateChange"; private static final String EVENT_RESTORED_RESULT = "appRestoredResult"; @@ -29,6 +40,7 @@ public class AppPlugin extends Plugin { private boolean hasPausedEver = false; private OnBackPressedCallback onBackPressedCallback; + private OnBackAnimationCallback onBackAnimationCallback; public void load() { boolean disableBackButtonHandler = getConfig().getBoolean("disableBackButtonHandler", false); @@ -63,6 +75,58 @@ public void handleOnBackPressed() { } }; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + this.onBackAnimationCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + if (hasListeners(EVENT_BACK_GESTURE)) { + JSObject data = new JSObject(); + data.put("phase", "commit"); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + } + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "start"); + data.put("progress", backEvent.getProgress()); + data.put("swipeEdge", backEvent.getSwipeEdge()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "progress"); + data.put("progress", backEvent.getProgress()); + data.put("swipeEdge", backEvent.getSwipeEdge()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + + @Override + public void onBackCancelled() { + OnBackAnimationCallback.super.onBackCancelled(); + + JSObject data = new JSObject(); + data.put("phase", "cancel"); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + } + }; + } + getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); } @@ -182,4 +246,13 @@ private void unsetAppListeners() { bridge.getApp().setStatusChangeListener(null); bridge.getApp().setAppRestoredListener(null); } + + private String getSwipeEdge(int edge) { + return switch (edge) { + case EDGE_LEFT -> "left"; + case EDGE_RIGHT -> "right"; + case EDGE_NONE -> "none"; + default -> "none"; + }; + } } diff --git a/app/src/definitions.ts b/app/src/definitions.ts index 1029be2ec..ef6ae6d58 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -144,6 +144,14 @@ export interface BackButtonListenerEvent { canGoBack: boolean; } +export interface BackGestureListenerEvent { + phase: 'start' | 'progress' | 'cancel' | 'commit'; + progress?: number; + swipeEdge?: 'left' | 'right' | 'none'; + touchX?: number; + touchY?: number; +} + export interface ToggleBackButtonHandlerOptions { /** * Indicates whether to enable or disable default back button handling. @@ -157,6 +165,7 @@ export type StateChangeListener = (state: AppState) => void; export type URLOpenListener = (event: URLOpenListenerEvent) => void; export type RestoredListener = (event: RestoredListenerEvent) => void; export type BackButtonListener = (event: BackButtonListenerEvent) => void; +export type BackGestureListener = (event: BackGestureListenerEvent) => void; export interface AppLanguageCode { /** @@ -303,6 +312,8 @@ export interface AppPlugin { */ addListener(eventName: 'backButton', listenerFunc: BackButtonListener): Promise; + addListener(eventName: 'backGesture', listenerFunc: BackGestureListener): Promise; + /** * Remove all native listeners for this plugin * From f7fd6fd208bfe65b48fae7423f5061de371ff3ce Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Fri, 5 Jun 2026 11:09:22 -0500 Subject: [PATCH 11/21] feat(app): add backGesture event for iOS back / forward edge gestures --- app/ios/Sources/AppPlugin/AppPlugin.swift | 84 +++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index c93b84a6d..45c7de55d 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -17,6 +17,8 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { private var observers: [NSObjectProtocol] = [] override public func load() { + loadGestureRecognizers() + NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.handleUniversalLink(notification:)), name: Notification.Name.capacitorOpenUniversalLink, object: nil) observers.append(NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] (_) in @@ -125,4 +127,86 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } + + @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { + guard let view = bridge?.webView else { return } + + let translation = recognizer.translation(in: webView) + let viewWidth = view.bounds.width + let viewHeight = view.bounds.height + + var data: [String: Any] = [:] + + switch recognizer.edges { + case .left: + let progress = translation.x / viewWidth + data["swipeEdge"] = "left" + data["progress"] = max(0, min(1, progress)) + + case .right: + let progress = -translation.x / viewWidth + data["swipeEdge"] = "right" + data["progress"] = max(0, min(1, progress)) + case .top: + let progress = translation.y / viewHeight + data["swipeEdge"] = "top" + data["progress"] = max(0, min(1, progress)) + case .bottom: + let progress = -translation.y / viewHeight + data["swipeEdge"] = "bottom" + data["progress"] = max(0, min(1, progress)) + case .all: + break + default: + break + } + + switch recognizer.state { + case .began: + data["phase"] = "start" + case .changed: + data["phase"] = "progress" + case .ended: + data["phase"] = "commit" + case .cancelled: + data["phase"] = "cancel" + case .failed: + data["phase"] = "cancel" + case .possible: + break + @unknown default: + break + } + + notifyListeners("backGesture", data: data) + } + + private func loadGestureRecognizers() { + guard let window = UIApplication.shared.delegate?.window ?? nil else { return } + + let leftEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleEdgePan(_:))) + leftEdgePanRecognizer.delegate = self + leftEdgePanRecognizer.edges = .left + + let rightEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleEdgePan(_:))) + rightEdgePanRecognizer.delegate = self + rightEdgePanRecognizer.edges = .right + + window.addGestureRecognizer(leftEdgePanRecognizer) + window.addGestureRecognizer(rightEdgePanRecognizer) + } +} + +extension AppPlugin: UIGestureRecognizerDelegate { + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } + + public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { + return false + } } From 88e595641a1b3997975b1248da717b930394d515 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Fri, 5 Jun 2026 12:32:02 -0500 Subject: [PATCH 12/21] feat(app): add runtime toggle for back gesture handler --- app/README.md | 21 +- .../capacitorjs/plugins/app/AppPlugin.java | 186 ++++++++++++------ app/ios/Sources/AppPlugin/AppPlugin.swift | 34 +++- app/src/definitions.ts | 4 + app/src/web.ts | 4 + 5 files changed, 188 insertions(+), 61 deletions(-) diff --git a/app/README.md b/app/README.md index 6913244b6..45111d74d 100644 --- a/app/README.md +++ b/app/README.md @@ -75,6 +75,7 @@ const checkAppLaunchUrl = async () => { | Prop | Type | Description | Default | Since | | ------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ------------------ | ----- | | **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | +| **`backGestureEnabled`** | boolean | | | | ### Examples @@ -84,7 +85,8 @@ In `capacitor.config.json`: { "plugins": { "App": { - "disableBackButtonHandler": true + "disableBackButtonHandler": true, + "backGestureEnabled": undefined } } } @@ -101,6 +103,7 @@ const config: CapacitorConfig = { plugins: { App: { disableBackButtonHandler: true, + backGestureEnabled: undefined, }, }, }; @@ -121,6 +124,7 @@ export default config; * [`minimizeApp()`](#minimizeapp) * [`getAppLanguage()`](#getapplanguage) * [`toggleBackButtonHandler(...)`](#togglebackbuttonhandler) +* [`setBackGestureEnabled(...)`](#setbackgestureenabled) * [`addListener('appStateChange', ...)`](#addlistenerappstatechange-) * [`addListener('pause', ...)`](#addlistenerpause-) * [`addListener('resume', ...)`](#addlistenerresume-) @@ -247,6 +251,19 @@ Only available for Android. -------------------- +### setBackGestureEnabled(...) + +```typescript +setBackGestureEnabled(options: { enabled: boolean; }) => Promise +``` + +| Param | Type | +| ------------- | ---------------------------------- | +| **`options`** | { enabled: boolean; } | + +-------------------- + + ### addListener('appStateChange', ...) ```typescript @@ -514,7 +531,7 @@ Remove all native listeners for this plugin | --------------- | ---------------------------------------------------------- | | **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | | **`progress`** | number | -| **`swipeEdge`** | 'left' \| 'right' | +| **`swipeEdge`** | 'none' \| 'left' \| 'right' | | **`touchX`** | number | | **`touchY`** | number | diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index f97870646..d6d1dc666 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -11,8 +11,7 @@ import android.os.Build; import android.window.BackEvent; import android.window.OnBackAnimationCallback; - -import androidx.activity.BackEventCompat; +import android.window.OnBackInvokedDispatcher; import androidx.activity.OnBackPressedCallback; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; @@ -38,12 +37,20 @@ public class AppPlugin extends Plugin { private static final String EVENT_PAUSE = "pause"; private static final String EVENT_RESUME = "resume"; private boolean hasPausedEver = false; + private boolean backButtonHandlerEnabled = false; + private boolean backGestureHandlerEnabled = false; private OnBackPressedCallback onBackPressedCallback; private OnBackAnimationCallback onBackAnimationCallback; + private String activeEdge = null; + private Float lastEdgeProgress = null; + private Float lastEdgeTouchX = null; + private Float lastEdgeTouchY = null; + public void load() { - boolean disableBackButtonHandler = getConfig().getBoolean("disableBackButtonHandler", false); + this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); + this.backGestureHandlerEnabled = getConfig().getBoolean("enableBackGestureHandler", false); bridge .getApp() @@ -59,7 +66,8 @@ public void load() { Logger.debug(getLogTag(), "Firing restored result"); notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); }); - this.onBackPressedCallback = new OnBackPressedCallback(!disableBackButtonHandler) { + + this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !backGestureHandlerEnabled) { @Override public void handleOnBackPressed() { if (!hasListeners(EVENT_BACK_BUTTON)) { @@ -75,56 +83,8 @@ public void handleOnBackPressed() { } }; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - this.onBackAnimationCallback = new OnBackAnimationCallback() { - @Override - public void onBackInvoked() { - if (hasListeners(EVENT_BACK_GESTURE)) { - JSObject data = new JSObject(); - data.put("phase", "commit"); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - } - - @Override - public void onBackStarted(@NonNull BackEvent backEvent) { - OnBackAnimationCallback.super.onBackStarted(backEvent); - JSObject data = new JSObject(); - data.put("phase", "start"); - data.put("progress", backEvent.getProgress()); - data.put("swipeEdge", backEvent.getSwipeEdge()); - data.put("touchX", backEvent.getTouchX()); - data.put("touchY", backEvent.getTouchY()); - data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - - @Override - public void onBackProgressed(@NonNull BackEvent backEvent) { - OnBackAnimationCallback.super.onBackStarted(backEvent); - JSObject data = new JSObject(); - data.put("phase", "progress"); - data.put("progress", backEvent.getProgress()); - data.put("swipeEdge", backEvent.getSwipeEdge()); - data.put("touchX", backEvent.getTouchX()); - data.put("touchY", backEvent.getTouchY()); - data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - - @Override - public void onBackCancelled() { - OnBackAnimationCallback.super.onBackCancelled(); - - JSObject data = new JSObject(); - data.put("phase", "cancel"); - - notifyListeners(EVENT_BACK_GESTURE, data, true); - } - }; + if (this.backGestureHandlerEnabled) { + this.setupBackGestureHandlers(); } getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); @@ -187,9 +147,10 @@ public void toggleBackButtonHandler(PluginCall call) { return; } - Boolean enabled = call.getBoolean("enabled"); + Boolean enabled = call.getBoolean("enabled", false); + backButtonHandlerEnabled = enabled; - this.onBackPressedCallback.setEnabled(enabled); + this.onBackPressedCallback.setEnabled(backButtonHandlerEnabled); call.resolve(); } @@ -202,6 +163,22 @@ public void getAppLanguage(PluginCall call) { call.resolve(ret); } + @PluginMethod + public void setBackGestureEnabled(PluginCall call) { + Boolean enabled = call.getBoolean("enabled", false); + backGestureHandlerEnabled = enabled; + + if (backGestureHandlerEnabled) { + this.onBackPressedCallback.setEnabled(false); + setupBackGestureHandlers(); + } else { + this.onBackPressedCallback.setEnabled(this.backButtonHandlerEnabled); + teardownBackGestureHandlers(); + } + + call.resolve(); + } + /** * Handle ACTION_VIEW intents to store a URL that was used to open the app * @param intent @@ -247,6 +224,103 @@ private void unsetAppListeners() { bridge.getApp().setAppRestoredListener(null); } + private void setupBackGestureHandlers() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + this.onBackAnimationCallback = new OnBackAnimationCallback() { + @Override + public void onBackInvoked() { + if (hasListeners(EVENT_BACK_GESTURE)) { + JSObject data = new JSObject(); + data.put("phase", "commit"); + data.put("progress", lastEdgeProgress); + data.put("touchX", lastEdgeTouchX); + data.put("touchY", lastEdgeTouchY); + data.put("swipeEdge", activeEdge); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = null; + lastEdgeTouchX = null; + lastEdgeTouchY = null; + activeEdge = null; + } + } + + @Override + public void onBackStarted(@NonNull BackEvent backEvent) { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "start"); + data.put("progress", backEvent.getProgress()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = backEvent.getProgress(); + lastEdgeTouchX = backEvent.getTouchX(); + lastEdgeTouchY = backEvent.getTouchY(); + activeEdge = getSwipeEdge(backEvent.getSwipeEdge()); + } + } + + @Override + public void onBackProgressed(@NonNull BackEvent backEvent) { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackStarted(backEvent); + JSObject data = new JSObject(); + data.put("phase", "progress"); + data.put("progress", backEvent.getProgress()); + data.put("touchX", backEvent.getTouchX()); + data.put("touchY", backEvent.getTouchY()); + data.put("swipeEdge", getSwipeEdge(backEvent.getSwipeEdge())); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = backEvent.getProgress(); + lastEdgeTouchX = backEvent.getTouchX(); + lastEdgeTouchY = backEvent.getTouchY(); + activeEdge = getSwipeEdge(backEvent.getSwipeEdge()); + } + } + + @Override + public void onBackCancelled() { + if (hasListeners(EVENT_BACK_GESTURE)) { + OnBackAnimationCallback.super.onBackCancelled(); + + JSObject data = new JSObject(); + data.put("phase", "cancel"); + data.put("progress", lastEdgeProgress); + data.put("touchX", lastEdgeTouchX); + data.put("touchY", lastEdgeTouchY); + data.put("swipeEdge", activeEdge); + + notifyListeners(EVENT_BACK_GESTURE, data, true); + + lastEdgeProgress = null; + lastEdgeTouchX = null; + lastEdgeTouchY = null; + activeEdge = null; + } + } + }; + + getActivity() + .getOnBackInvokedDispatcher() + .registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, this.onBackAnimationCallback); + } + } + + private void teardownBackGestureHandlers() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + getActivity().getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(this.onBackAnimationCallback); + this.onBackAnimationCallback = null; + } + } + private String getSwipeEdge(int edge) { return switch (edge) { case EDGE_LEFT -> "left"; diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 45c7de55d..df7738386 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -12,12 +12,17 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "getLaunchUrl", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "minimizeApp", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setBackGestureEnabled", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] - + private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil + private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil + override public func load() { - loadGestureRecognizers() + if getConfig().getBoolean("enableBackGestureHandler", false) { + loadGestureRecognizers() + } NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(self.handleUniversalLink(notification:)), name: Notification.Name.capacitorOpenUniversalLink, object: nil) @@ -127,6 +132,16 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } + + @objc func setBackGestureEnabled(_ call: CAPPluginCall) { + if (call.getBool("enabled", false)) { + loadGestureRecognizers() + } else { + destroyGestureRecognizers() + } + + call.resolve() + } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { guard let view = bridge?.webView else { return } @@ -194,6 +209,19 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { window.addGestureRecognizer(leftEdgePanRecognizer) window.addGestureRecognizer(rightEdgePanRecognizer) + + self.leftEdgePanRecognizer = leftEdgePanRecognizer + self.rightEdgePanRecognizer = rightEdgePanRecognizer + } + + private func destroyGestureRecognizers() { + guard let window = UIApplication.shared.delegate?.window ?? nil, + let leftEdgePanRecognizer = self.leftEdgePanRecognizer, + let rightEdgePanRecognizer = self.rightEdgePanRecognizer + else { return } + + window.removeGestureRecognizer(leftEdgePanRecognizer) + window.removeGestureRecognizer(rightEdgePanRecognizer) } } diff --git a/app/src/definitions.ts b/app/src/definitions.ts index ef6ae6d58..6ba79b287 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -15,6 +15,8 @@ declare module '@capacitor/cli' { * @example true */ disableBackButtonHandler?: boolean; + + backGestureEnabled?: boolean; }; } } @@ -233,6 +235,8 @@ export interface AppPlugin { */ toggleBackButtonHandler(options: ToggleBackButtonHandlerOptions): Promise; + setBackGestureEnabled(options: { enabled: boolean }): Promise; + /** * Listen for changes in the app or the activity states. * diff --git a/app/src/web.ts b/app/src/web.ts index be23cc767..585024538 100644 --- a/app/src/web.ts +++ b/app/src/web.ts @@ -32,6 +32,10 @@ export class AppWeb extends WebPlugin implements AppPlugin { throw this.unimplemented('Not implemented on web.'); } + async setBackGestureEnabled(): Promise { + throw new Error('Method not implemented.'); + } + private handleVisibilityChange = () => { const data = { isActive: document.hidden !== true, From 4300be33ec0dd6394b8dc54cc37fb848c30ab071 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:04 -0500 Subject: [PATCH 13/21] refactor(app): rename backGesture API to edgeGesture --- app/README.md | 41 +++++++++++-------- .../capacitorjs/plugins/app/AppPlugin.java | 16 ++++---- app/ios/Sources/AppPlugin/AppPlugin.swift | 22 +++++----- app/src/definitions.ts | 14 ++++--- app/src/web.ts | 2 +- 5 files changed, 54 insertions(+), 41 deletions(-) diff --git a/app/README.md b/app/README.md index 45111d74d..6ad5c8964 100644 --- a/app/README.md +++ b/app/README.md @@ -75,7 +75,7 @@ const checkAppLaunchUrl = async () => { | Prop | Type | Description | Default | Since | | ------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ------------------ | ----- | | **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | -| **`backGestureEnabled`** | boolean | | | | +| **`enableEdgeGestureHandler`** | boolean | | | | ### Examples @@ -86,7 +86,7 @@ In `capacitor.config.json`: "plugins": { "App": { "disableBackButtonHandler": true, - "backGestureEnabled": undefined + "enableEdgeGestureHandler": undefined } } } @@ -103,7 +103,7 @@ const config: CapacitorConfig = { plugins: { App: { disableBackButtonHandler: true, - backGestureEnabled: undefined, + enableEdgeGestureHandler: undefined, }, }, }; @@ -124,14 +124,14 @@ export default config; * [`minimizeApp()`](#minimizeapp) * [`getAppLanguage()`](#getapplanguage) * [`toggleBackButtonHandler(...)`](#togglebackbuttonhandler) -* [`setBackGestureEnabled(...)`](#setbackgestureenabled) +* [`toggleEdgeGestureHandler(...)`](#toggleedgegesturehandler) * [`addListener('appStateChange', ...)`](#addlistenerappstatechange-) * [`addListener('pause', ...)`](#addlistenerpause-) * [`addListener('resume', ...)`](#addlistenerresume-) * [`addListener('appUrlOpen', ...)`](#addlistenerappurlopen-) * [`addListener('appRestoredResult', ...)`](#addlistenerapprestoredresult-) * [`addListener('backButton', ...)`](#addlistenerbackbutton-) -* [`addListener('backGesture', ...)`](#addlistenerbackgesture-) +* [`addListener('edgeGesture', ...)`](#addlisteneredgegesture-) * [`removeAllListeners()`](#removealllisteners) * [Interfaces](#interfaces) * [Type Aliases](#type-aliases) @@ -251,15 +251,15 @@ Only available for Android. -------------------- -### setBackGestureEnabled(...) +### toggleEdgeGestureHandler(...) ```typescript -setBackGestureEnabled(options: { enabled: boolean; }) => Promise +toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions) => Promise ``` -| Param | Type | -| ------------- | ---------------------------------- | -| **`options`** | { enabled: boolean; } | +| Param | Type | +| ------------- | ------------------------------------------------------------------------------------------- | +| **`options`** | ToggleEdgeGestureHandlerOptions | -------------------- @@ -421,16 +421,16 @@ If you want to close the app, call `App.exitApp()`. -------------------- -### addListener('backGesture', ...) +### addListener('edgeGesture', ...) ```typescript -addListener(eventName: 'backGesture', listenerFunc: BackGestureListener) => Promise +addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener) => Promise ``` | Param | Type | | ------------------ | ------------------------------------------------------------------- | -| **`eventName`** | 'backGesture' | -| **`listenerFunc`** | BackGestureListener | +| **`eventName`** | 'edgeGesture' | +| **`listenerFunc`** | EdgeGestureListener | **Returns:** Promise<PluginListenerHandle> @@ -491,6 +491,13 @@ Remove all native listeners for this plugin | **`enabled`** | boolean | Indicates whether to enable or disable default back button handling. | 7.1.0 | +#### ToggleEdgeGestureHandlerOptions + +| Prop | Type | +| ------------- | -------------------- | +| **`enabled`** | boolean | + + #### PluginListenerHandle | Prop | Type | @@ -525,7 +532,7 @@ Remove all native listeners for this plugin | **`canGoBack`** | boolean | Indicates whether the browser can go back in history. False when the history stack is on the first entry. | 1.0.0 | -#### BackGestureListenerEvent +#### EdgeGestureListenerEvent | Prop | Type | | --------------- | ---------------------------------------------------------- | @@ -559,8 +566,8 @@ Remove all native listeners for this plugin (event: BackButtonListenerEvent): void -#### BackGestureListener +#### EdgeGestureListener -(event: BackGestureListenerEvent): void +(event: EdgeGestureListenerEvent): void diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index d6d1dc666..028b06612 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -30,7 +30,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_BACK_BUTTON = "backButton"; - private static final String EVENT_BACK_GESTURE = "backGesture"; + private static final String EVENT_BACK_GESTURE = "edgeGesture"; private static final String EVENT_URL_OPEN = "appUrlOpen"; private static final String EVENT_STATE_CHANGE = "appStateChange"; private static final String EVENT_RESTORED_RESULT = "appRestoredResult"; @@ -38,7 +38,7 @@ public class AppPlugin extends Plugin { private static final String EVENT_RESUME = "resume"; private boolean hasPausedEver = false; private boolean backButtonHandlerEnabled = false; - private boolean backGestureHandlerEnabled = false; + private boolean edgeGestureHandlerEnabled = false; private OnBackPressedCallback onBackPressedCallback; private OnBackAnimationCallback onBackAnimationCallback; @@ -50,7 +50,7 @@ public class AppPlugin extends Plugin { public void load() { this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); - this.backGestureHandlerEnabled = getConfig().getBoolean("enableBackGestureHandler", false); + this.edgeGestureHandlerEnabled = getConfig().getBoolean("enableEdgeGestureHandler", false); bridge .getApp() @@ -67,7 +67,7 @@ public void load() { notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); }); - this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !backGestureHandlerEnabled) { + this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !edgeGestureHandlerEnabled) { @Override public void handleOnBackPressed() { if (!hasListeners(EVENT_BACK_BUTTON)) { @@ -83,7 +83,7 @@ public void handleOnBackPressed() { } }; - if (this.backGestureHandlerEnabled) { + if (this.edgeGestureHandlerEnabled) { this.setupBackGestureHandlers(); } @@ -164,11 +164,11 @@ public void getAppLanguage(PluginCall call) { } @PluginMethod - public void setBackGestureEnabled(PluginCall call) { + public void toggleEdgeGestureHandler(PluginCall call) { Boolean enabled = call.getBoolean("enabled", false); - backGestureHandlerEnabled = enabled; + edgeGestureHandlerEnabled = enabled; - if (backGestureHandlerEnabled) { + if (edgeGestureHandlerEnabled) { this.onBackPressedCallback.setEnabled(false); setupBackGestureHandlers(); } else { diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index df7738386..98ae34248 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -13,14 +13,14 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "minimizeApp", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "toggleBackButtonHandler", returnType: CAPPluginReturnPromise), - CAPPluginMethod(name: "setBackGestureEnabled", returnType: CAPPluginReturnPromise) + CAPPluginMethod(name: "toggleEdgeGestureHandler", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil override public func load() { - if getConfig().getBoolean("enableBackGestureHandler", false) { + if getConfig().getBoolean("enableEdgeGestureHandler", false) { loadGestureRecognizers() } @@ -133,14 +133,16 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { call.unimplemented() } - @objc func setBackGestureEnabled(_ call: CAPPluginCall) { - if (call.getBool("enabled", false)) { - loadGestureRecognizers() - } else { - destroyGestureRecognizers() + @objc func toggleEdgeGestureHandler(_ call: CAPPluginCall) { + DispatchQueue.main.async { + if (call.getBool("enabled", false)) { + self.loadGestureRecognizers() + } else { + self.destroyGestureRecognizers() + } + + call.resolve() } - - call.resolve() } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { @@ -193,7 +195,7 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { break } - notifyListeners("backGesture", data: data) + notifyListeners("edgeGesture", data: data) } private func loadGestureRecognizers() { diff --git a/app/src/definitions.ts b/app/src/definitions.ts index 6ba79b287..f61ff5660 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -16,7 +16,7 @@ declare module '@capacitor/cli' { */ disableBackButtonHandler?: boolean; - backGestureEnabled?: boolean; + enableEdgeGestureHandler?: boolean; }; } } @@ -146,7 +146,7 @@ export interface BackButtonListenerEvent { canGoBack: boolean; } -export interface BackGestureListenerEvent { +export interface EdgeGestureListenerEvent { phase: 'start' | 'progress' | 'cancel' | 'commit'; progress?: number; swipeEdge?: 'left' | 'right' | 'none'; @@ -163,11 +163,15 @@ export interface ToggleBackButtonHandlerOptions { enabled: boolean; } +export interface ToggleEdgeGestureHandlerOptions { + enabled: boolean; +} + export type StateChangeListener = (state: AppState) => void; export type URLOpenListener = (event: URLOpenListenerEvent) => void; export type RestoredListener = (event: RestoredListenerEvent) => void; export type BackButtonListener = (event: BackButtonListenerEvent) => void; -export type BackGestureListener = (event: BackGestureListenerEvent) => void; +export type EdgeGestureListener = (event: EdgeGestureListenerEvent) => void; export interface AppLanguageCode { /** @@ -235,7 +239,7 @@ export interface AppPlugin { */ toggleBackButtonHandler(options: ToggleBackButtonHandlerOptions): Promise; - setBackGestureEnabled(options: { enabled: boolean }): Promise; + toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions): Promise; /** * Listen for changes in the app or the activity states. @@ -316,7 +320,7 @@ export interface AppPlugin { */ addListener(eventName: 'backButton', listenerFunc: BackButtonListener): Promise; - addListener(eventName: 'backGesture', listenerFunc: BackGestureListener): Promise; + addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener): Promise; /** * Remove all native listeners for this plugin diff --git a/app/src/web.ts b/app/src/web.ts index 585024538..ca68695f9 100644 --- a/app/src/web.ts +++ b/app/src/web.ts @@ -32,7 +32,7 @@ export class AppWeb extends WebPlugin implements AppPlugin { throw this.unimplemented('Not implemented on web.'); } - async setBackGestureEnabled(): Promise { + async toggleEdgeGestureHandler(): Promise { throw new Error('Method not implemented.'); } From 96d46ddf2dacb8cbbcddc0314111bf76ab4ac827 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:26 -0500 Subject: [PATCH 14/21] feat(app): include touch coordinates in iOS edge gesture event --- app/ios/Sources/AppPlugin/AppPlugin.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 98ae34248..8ddc6fca5 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -149,10 +149,14 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { guard let view = bridge?.webView else { return } let translation = recognizer.translation(in: webView) + let touch = recognizer.location(in: view) let viewWidth = view.bounds.width let viewHeight = view.bounds.height var data: [String: Any] = [:] + + data["touchX"] = touch.x + data["touchY"] = touch.y switch recognizer.edges { case .left: From 9af482dc8fbe5b47fd1a65b8a39f0993c5739a18 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:22:49 -0500 Subject: [PATCH 15/21] fmt --- app/ios/Sources/AppPlugin/AppPlugin.swift | 44 +++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 8ddc6fca5..4c54987c3 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -16,12 +16,12 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "toggleEdgeGestureHandler", returnType: CAPPluginReturnPromise) ] private var observers: [NSObjectProtocol] = [] - private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil - private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? = nil - + private var leftEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? + private var rightEdgePanRecognizer: UIScreenEdgePanGestureRecognizer? + override public func load() { if getConfig().getBoolean("enableEdgeGestureHandler", false) { - loadGestureRecognizers() + loadGestureRecognizers() } NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) @@ -132,17 +132,17 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func toggleBackButtonHandler(_ call: CAPPluginCall) { call.unimplemented() } - + @objc func toggleEdgeGestureHandler(_ call: CAPPluginCall) { - DispatchQueue.main.async { - if (call.getBool("enabled", false)) { - self.loadGestureRecognizers() - } else { - self.destroyGestureRecognizers() + DispatchQueue.main.async { + if call.getBool("enabled", false) { + self.loadGestureRecognizers() + } else { + self.destroyGestureRecognizers() + } + + call.resolve() } - - call.resolve() - } } @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { @@ -154,7 +154,7 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { let viewHeight = view.bounds.height var data: [String: Any] = [:] - + data["touchX"] = touch.x data["touchY"] = touch.y @@ -215,19 +215,19 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { window.addGestureRecognizer(leftEdgePanRecognizer) window.addGestureRecognizer(rightEdgePanRecognizer) - + self.leftEdgePanRecognizer = leftEdgePanRecognizer self.rightEdgePanRecognizer = rightEdgePanRecognizer } - + private func destroyGestureRecognizers() { - guard let window = UIApplication.shared.delegate?.window ?? nil, - let leftEdgePanRecognizer = self.leftEdgePanRecognizer, - let rightEdgePanRecognizer = self.rightEdgePanRecognizer + guard let window = UIApplication.shared.delegate?.window ?? nil, + let leftEdgePanRecognizer = self.leftEdgePanRecognizer, + let rightEdgePanRecognizer = self.rightEdgePanRecognizer else { return } - - window.removeGestureRecognizer(leftEdgePanRecognizer) - window.removeGestureRecognizer(rightEdgePanRecognizer) + + window.removeGestureRecognizer(leftEdgePanRecognizer) + window.removeGestureRecognizer(rightEdgePanRecognizer) } } From 7384dd3eaaed095f30d0dd86f69055c8a4d4b7e2 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 14:52:57 -0500 Subject: [PATCH 16/21] Revert "Merge branch 'RMET-5179' of github.com:ionic-team/capacitor-plugins into RMET-5179" This reverts commit bc5eecce8cd3d827ec5b01908ad516ddbad7c227, reversing changes made to 9af482dc8fbe5b47fd1a65b8a39f0993c5739a18. --- push-notifications/CHANGELOG.md | 4 -- push-notifications/package.json | 2 +- .../plugins/splashscreen/SplashScreen.java | 49 +++++++++++-------- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/push-notifications/CHANGELOG.md b/push-notifications/CHANGELOG.md index 59002744f..307b91207 100644 --- a/push-notifications/CHANGELOG.md +++ b/push-notifications/CHANGELOG.md @@ -3,10 +3,6 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. -## [8.1.1](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.1.0...@capacitor/push-notifications@8.1.1) (2026-05-15) - -**Note:** Version bump only for package @capacitor/push-notifications - # [8.1.0](https://github.com/ionic-team/capacitor-plugins/compare/@capacitor/push-notifications@8.0.4...@capacitor/push-notifications@8.1.0) (2026-05-15) ### Features diff --git a/push-notifications/package.json b/push-notifications/package.json index 82c999a56..51eba6b0a 100644 --- a/push-notifications/package.json +++ b/push-notifications/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor/push-notifications", - "version": "8.1.1", + "version": "8.1.0", "description": "The Push Notifications API provides access to native push notifications.", "main": "dist/plugin.cjs.js", "module": "dist/esm/index.js", diff --git a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java index 756a2d6d5..af87b3ecf 100644 --- a/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java +++ b/splash-screen/android/src/main/java/com/capacitorjs/plugins/splashscreen/SplashScreen.java @@ -138,14 +138,17 @@ public boolean onPreDraw() { if (!isVisible && !isHiding) { isVisible = true; - new Handler(context.getMainLooper()).postDelayed(() -> { - // Splash screen is done... start drawing content. - if (settings.isAutoHide()) { - isVisible = false; - onPreDrawListener = null; - content.getViewTreeObserver().removeOnPreDrawListener(this); - } - }, settings.getShowDuration()); + new Handler(context.getMainLooper()).postDelayed( + () -> { + // Splash screen is done... start drawing content. + if (settings.isAutoHide()) { + isVisible = false; + onPreDrawListener = null; + content.getViewTreeObserver().removeOnPreDrawListener(this); + } + }, + settings.getShowDuration() + ); } // Not ready to dismiss splash screen @@ -222,13 +225,16 @@ private void showDialog( isVisible = true; if (settings.isAutoHide()) { - new Handler(context.getMainLooper()).postDelayed(() -> { - hideDialog(activity, isLaunchSplash); + new Handler(context.getMainLooper()).postDelayed( + () -> { + hideDialog(activity, isLaunchSplash); - if (splashListener != null) { - splashListener.completed(); - } - }, settings.getShowDuration()); + if (splashListener != null) { + splashListener.completed(); + } + }, + settings.getShowDuration() + ); } else { // If no autoHide, call complete if (splashListener != null) { @@ -391,13 +397,16 @@ public void onAnimationEnd(Animator animator) { isVisible = true; if (settings.isAutoHide()) { - new Handler(context.getMainLooper()).postDelayed(() -> { - hide(settings.getFadeOutDuration(), isLaunchSplash); + new Handler(context.getMainLooper()).postDelayed( + () -> { + hide(settings.getFadeOutDuration(), isLaunchSplash); - if (splashListener != null) { - splashListener.completed(); - } - }, settings.getShowDuration()); + if (splashListener != null) { + splashListener.completed(); + } + }, + settings.getShowDuration() + ); } else { // If no autoHide, call complete if (splashListener != null) { From 7f57dfb2d3677ebe8cdd1a23de83b83e3133b522 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Mon, 8 Jun 2026 15:15:49 -0500 Subject: [PATCH 17/21] docs(app): document edge gesture handler API --- app/README.md | 62 +++++++++++++++++------- app/src/definitions.ts | 105 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 16 deletions(-) diff --git a/app/README.md b/app/README.md index 6ad5c8964..78ce16d4a 100644 --- a/app/README.md +++ b/app/README.md @@ -72,10 +72,10 @@ const checkAppLaunchUrl = async () => { -| Prop | Type | Description | Default | Since | -| ------------------------------ | -------------------- | ------------------------------------------------------------------------------ | ------------------ | ----- | -| **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | -| **`enableEdgeGestureHandler`** | boolean | | | | +| Prop | Type | Description | Default | Since | +| ------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | +| **`enableEdgeGestureHandler`** | boolean | Enable the plugin's edge gesture handler at startup. When enabled, the plugin emits `edgeGesture` events for system edge swipes (iOS left/right screen-edge pans, Android predictive back). On Android, enabling this handler suppresses the default `backButton` handler for the duration that the edge gesture handler is active. The Android predictive-back integration requires API 34 (Android 14) or later; on earlier versions the configuration is accepted but no events will be emitted. | false | 9.0.0 | ### Examples @@ -86,7 +86,7 @@ In `capacitor.config.json`: "plugins": { "App": { "disableBackButtonHandler": true, - "enableEdgeGestureHandler": undefined + "enableEdgeGestureHandler": true } } } @@ -103,7 +103,7 @@ const config: CapacitorConfig = { plugins: { App: { disableBackButtonHandler: true, - enableEdgeGestureHandler: undefined, + enableEdgeGestureHandler: true, }, }, }; @@ -257,10 +257,24 @@ Only available for Android. toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions) => Promise ``` +Enables or disables the plugin's edge gesture handling at runtime. + +When enabled, the plugin installs platform edge-gesture recognizers +and begins emitting `edgeGesture` events. When disabled, the +recognizers are removed and no further events are emitted. + +On Android, enabling the edge gesture handler temporarily disables +the default `backButton` handler; disabling it restores the previous +back button handler state. The Android predictive-back integration +requires API 34 (Android 14) or later; on earlier versions the call +resolves but no events will be emitted. + | Param | Type | | ------------- | ------------------------------------------------------------------------------------------- | | **`options`** | ToggleEdgeGestureHandlerOptions | +**Since:** 9.0.0 + -------------------- @@ -427,6 +441,20 @@ If you want to close the app, call `App.exitApp()`. addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener) => Promise ``` +Listen for system edge-swipe gestures. + +On iOS this fires for left- and right-edge screen pans tracked by +`UIScreenEdgePanGestureRecognizer`. On Android this fires for the +predictive back gesture (requires Android 14 / API 34 or later). + +The edge gesture handler must be active for events to fire; enable +it via the `enableEdgeGestureHandler` configuration option or at +runtime via `toggleEdgeGestureHandler({ enabled: true })`. + +Each gesture produces a sequence of events: a single `start`, zero +or more `progress`, and then either `commit` (the gesture completed) +or `cancel` (the gesture was abandoned). + | Param | Type | | ------------------ | ------------------------------------------------------------------- | | **`eventName`** | 'edgeGesture' | @@ -434,6 +462,8 @@ addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener) => Prom **Returns:** Promise<PluginListenerHandle> +**Since:** 9.0.0 + -------------------- @@ -493,9 +523,9 @@ Remove all native listeners for this plugin #### ToggleEdgeGestureHandlerOptions -| Prop | Type | -| ------------- | -------------------- | -| **`enabled`** | boolean | +| Prop | Type | Description | Since | +| ------------- | -------------------- | ---------------------------------------------------------------- | ----- | +| **`enabled`** | boolean | Whether to enable or disable the plugin's edge gesture handling. | 9.0.0 | #### PluginListenerHandle @@ -534,13 +564,13 @@ Remove all native listeners for this plugin #### EdgeGestureListenerEvent -| Prop | Type | -| --------------- | ---------------------------------------------------------- | -| **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | -| **`progress`** | number | -| **`swipeEdge`** | 'none' \| 'left' \| 'right' | -| **`touchX`** | number | -| **`touchY`** | number | +| Prop | Type | Description | Since | +| --------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| **`phase`** | 'start' \| 'progress' \| 'cancel' \| 'commit' | The current phase of the edge gesture. - `start`: the user has initiated an edge swipe. - `progress`: the user is moving their finger; emitted continuously during the gesture. - `commit`: the user released the gesture and the system accepted it (for example, a back navigation should occur). - `cancel`: the user released the gesture without committing it, or the system cancelled it. | 9.0.0 | +| **`progress`** | number | How far the gesture has progressed, normalized between `0` and `1`. On `start` this is the initial progress reported by the system. On `progress` it updates as the user drags. On `commit` and `cancel` it reports the last observed progress value. | 9.0.0 | +| **`swipeEdge`** | 'none' \| 'left' \| 'right' | Which screen edge the gesture originated from. On iOS this is `'left'` or `'right'` (left/right screen-edge pans are tracked). On Android this reflects the value reported by the predictive-back system and may also be `'none'` when the platform does not report a specific edge. | 9.0.0 | +| **`touchX`** | number | X coordinate of the touch that initiated or is driving the gesture. On iOS the value is in points relative to the WebView. On Android the value is provided by the platform's `BackEvent.getTouchX()`. | 9.0.0 | +| **`touchY`** | number | Y coordinate of the touch that initiated or is driving the gesture. On iOS the value is in points relative to the WebView. On Android the value is provided by the platform's `BackEvent.getTouchY()`. | 9.0.0 | ### Type Aliases diff --git a/app/src/definitions.ts b/app/src/definitions.ts index f61ff5660..40621219c 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -16,6 +16,22 @@ declare module '@capacitor/cli' { */ disableBackButtonHandler?: boolean; + /** + * Enable the plugin's edge gesture handler at startup. + * + * When enabled, the plugin emits `edgeGesture` events for system edge + * swipes (iOS left/right screen-edge pans, Android predictive back). + * + * On Android, enabling this handler suppresses the default + * `backButton` handler for the duration that the edge gesture handler + * is active. The Android predictive-back integration requires API 34 + * (Android 14) or later; on earlier versions the configuration is + * accepted but no events will be emitted. + * + * @since 9.0.0 + * @default false + * @example true + */ enableEdgeGestureHandler?: boolean; }; } @@ -147,10 +163,62 @@ export interface BackButtonListenerEvent { } export interface EdgeGestureListenerEvent { + /** + * The current phase of the edge gesture. + * + * - `start`: the user has initiated an edge swipe. + * - `progress`: the user is moving their finger; emitted continuously + * during the gesture. + * - `commit`: the user released the gesture and the system accepted it + * (for example, a back navigation should occur). + * - `cancel`: the user released the gesture without committing it, or + * the system cancelled it. + * + * @since 9.0.0 + */ phase: 'start' | 'progress' | 'cancel' | 'commit'; + + /** + * How far the gesture has progressed, normalized between `0` and `1`. + * + * On `start` this is the initial progress reported by the system. On + * `progress` it updates as the user drags. On `commit` and `cancel` + * it reports the last observed progress value. + * + * @since 9.0.0 + */ progress?: number; + + /** + * Which screen edge the gesture originated from. + * + * On iOS this is `'left'` or `'right'` (left/right screen-edge pans + * are tracked). On Android this reflects the value reported by the + * predictive-back system and may also be `'none'` when the platform + * does not report a specific edge. + * + * @since 9.0.0 + */ swipeEdge?: 'left' | 'right' | 'none'; + + /** + * X coordinate of the touch that initiated or is driving the gesture. + * + * On iOS the value is in points relative to the WebView. On Android the + * value is provided by the platform's `BackEvent.getTouchX()`. + * + * @since 9.0.0 + */ touchX?: number; + + /** + * Y coordinate of the touch that initiated or is driving the gesture. + * + * On iOS the value is in points relative to the WebView. On Android the + * value is provided by the platform's `BackEvent.getTouchY()`. + * + * @since 9.0.0 + */ touchY?: number; } @@ -164,6 +232,11 @@ export interface ToggleBackButtonHandlerOptions { } export interface ToggleEdgeGestureHandlerOptions { + /** + * Whether to enable or disable the plugin's edge gesture handling. + * + * @since 9.0.0 + */ enabled: boolean; } @@ -239,6 +312,21 @@ export interface AppPlugin { */ toggleBackButtonHandler(options: ToggleBackButtonHandlerOptions): Promise; + /** + * Enables or disables the plugin's edge gesture handling at runtime. + * + * When enabled, the plugin installs platform edge-gesture recognizers + * and begins emitting `edgeGesture` events. When disabled, the + * recognizers are removed and no further events are emitted. + * + * On Android, enabling the edge gesture handler temporarily disables + * the default `backButton` handler; disabling it restores the previous + * back button handler state. The Android predictive-back integration + * requires API 34 (Android 14) or later; on earlier versions the call + * resolves but no events will be emitted. + * + * @since 9.0.0 + */ toggleEdgeGestureHandler(options: ToggleEdgeGestureHandlerOptions): Promise; /** @@ -320,6 +408,23 @@ export interface AppPlugin { */ addListener(eventName: 'backButton', listenerFunc: BackButtonListener): Promise; + /** + * Listen for system edge-swipe gestures. + * + * On iOS this fires for left- and right-edge screen pans tracked by + * `UIScreenEdgePanGestureRecognizer`. On Android this fires for the + * predictive back gesture (requires Android 14 / API 34 or later). + * + * The edge gesture handler must be active for events to fire; enable + * it via the `enableEdgeGestureHandler` configuration option or at + * runtime via `toggleEdgeGestureHandler({ enabled: true })`. + * + * Each gesture produces a sequence of events: a single `start`, zero + * or more `progress`, and then either `commit` (the gesture completed) + * or `cancel` (the gesture was abandoned). + * + * @since 9.0.0 + */ addListener(eventName: 'edgeGesture', listenerFunc: EdgeGestureListener): Promise; /** From ada3c585b621e5d3b2c5236fa79511a000534620 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Tue, 9 Jun 2026 12:37:15 -0500 Subject: [PATCH 18/21] fmt --- .../capacitorjs/plugins/app/AppPlugin.java | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index 028b06612..51538f0d4 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -52,20 +52,16 @@ public void load() { this.backButtonHandlerEnabled = !getConfig().getBoolean("disableBackButtonHandler", false); this.edgeGestureHandlerEnabled = getConfig().getBoolean("enableEdgeGestureHandler", false); - bridge - .getApp() - .setStatusChangeListener((isActive) -> { - Logger.debug(getLogTag(), "Firing change: " + isActive); - JSObject data = new JSObject(); - data.put("isActive", isActive); - notifyListeners(EVENT_STATE_CHANGE, data, false); - }); - bridge - .getApp() - .setAppRestoredListener((result) -> { - Logger.debug(getLogTag(), "Firing restored result"); - notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); - }); + bridge.getApp().setStatusChangeListener((isActive) -> { + Logger.debug(getLogTag(), "Firing change: " + isActive); + JSObject data = new JSObject(); + data.put("isActive", isActive); + notifyListeners(EVENT_STATE_CHANGE, data, false); + }); + bridge.getApp().setAppRestoredListener((result) -> { + Logger.debug(getLogTag(), "Firing restored result"); + notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); + }); this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !edgeGestureHandlerEnabled) { @Override From 94b13bdcd3993462299bace916a5e10f8e0b15c4 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Thu, 11 Jun 2026 10:59:09 -0500 Subject: [PATCH 19/21] refinements and bug fixes for Android --- app/README.md | 16 +++-- .../capacitorjs/plugins/app/AppPlugin.java | 60 ++++++++++++------- app/src/definitions.ts | 10 +++- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/app/README.md b/app/README.md index 78ce16d4a..1b68431b2 100644 --- a/app/README.md +++ b/app/README.md @@ -43,6 +43,9 @@ For being able to open the app from a custom scheme you need to register the sch `custom_url_scheme` value is stored in `strings.xml`. When the Android platform is added, `@capacitor/cli` adds the app's package name as default value, but can be replaced by editing the `strings.xml` file. +## Android Predictive Back +Android predictive back also requires `android:enableOnBackInvokedCallback="true"` on `` in your `AndroidManifest.xml` (on Android 14; default on Android 15+). + ## Example ```typescript @@ -72,10 +75,10 @@ const checkAppLaunchUrl = async () => { -| Prop | Type | Description | Default | Since | -| ------------------------------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | -| **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | -| **`enableEdgeGestureHandler`** | boolean | Enable the plugin's edge gesture handler at startup. When enabled, the plugin emits `edgeGesture` events for system edge swipes (iOS left/right screen-edge pans, Android predictive back). On Android, enabling this handler suppresses the default `backButton` handler for the duration that the edge gesture handler is active. The Android predictive-back integration requires API 34 (Android 14) or later; on earlier versions the configuration is accepted but no events will be emitted. | false | 9.0.0 | +| Prop | Type | Description | Default | Since | +| ------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`disableBackButtonHandler`** | boolean | Disable the plugin's default back button handling. Only available for Android. | false | 7.1.0 | +| **`enableEdgeGestureHandler`** | boolean | Enable the plugin's edge gesture handler at startup. When enabled, the plugin emits `edgeGesture` events for system edge swipes (iOS left/right screen-edge pans, Android predictive back). On Android, enabling this handler suppresses the default `backButton` handler for the duration that the edge gesture handler is active. The Android predictive-back integration requires API 34 (Android 14) or later; on earlier versions the configuration is accepted but no events will be emitted. Android predictive back also requires `android:enableOnBackInvokedCallback="true"` on `<application>` in your `AndroidManifest.xml` (on Android 14; default on Android 15+). | false | 9.0.0 | ### Examples @@ -267,7 +270,10 @@ On Android, enabling the edge gesture handler temporarily disables the default `backButton` handler; disabling it restores the previous back button handler state. The Android predictive-back integration requires API 34 (Android 14) or later; on earlier versions the call -resolves but no events will be emitted. +resolves but no events will be emitted. Android predictive back also +requires `android:enableOnBackInvokedCallback="true"` on +`<application>` in your `AndroidManifest.xml` (on Android 14; default +on Android 15+). | Param | Type | | ------------- | ------------------------------------------------------------------------------------------- | diff --git a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java index 51538f0d4..18268e39b 100644 --- a/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java +++ b/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java @@ -63,7 +63,7 @@ public void load() { notifyListeners(EVENT_RESTORED_RESULT, result.getWrappedResult(), true); }); - this.onBackPressedCallback = new OnBackPressedCallback(backButtonHandlerEnabled && !edgeGestureHandlerEnabled) { + this.onBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { if (!hasListeners(EVENT_BACK_BUTTON)) { @@ -79,11 +79,13 @@ public void handleOnBackPressed() { } }; + getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); + if (this.edgeGestureHandlerEnabled) { this.setupBackGestureHandlers(); } - getActivity().getOnBackPressedDispatcher().addCallback(getActivity(), this.onBackPressedCallback); + applyBackButtonHandlerState(); } @PluginMethod @@ -136,6 +138,15 @@ public void minimizeApp(PluginCall call) { call.resolve(); } + @PluginMethod + public void getAppLanguage(PluginCall call) { + JSObject ret = new JSObject(); + LocaleListCompat appLocales = AppCompatDelegate.getApplicationLocales(); + Locale appLocale = !appLocales.isEmpty() ? appLocales.get(0) : null; + ret.put("value", appLocale != null ? appLocale.getLanguage() : Locale.getDefault().getLanguage()); + call.resolve(ret); + } + @PluginMethod public void toggleBackButtonHandler(PluginCall call) { if (this.onBackPressedCallback == null) { @@ -143,38 +154,37 @@ public void toggleBackButtonHandler(PluginCall call) { return; } - Boolean enabled = call.getBoolean("enabled", false); - backButtonHandlerEnabled = enabled; + backButtonHandlerEnabled = call.getBoolean("enabled", false); - this.onBackPressedCallback.setEnabled(backButtonHandlerEnabled); + applyBackButtonHandlerState(); call.resolve(); } - @PluginMethod - public void getAppLanguage(PluginCall call) { - JSObject ret = new JSObject(); - LocaleListCompat appLocales = AppCompatDelegate.getApplicationLocales(); - Locale appLocale = !appLocales.isEmpty() ? appLocales.get(0) : null; - ret.put("value", appLocale != null ? appLocale.getLanguage() : Locale.getDefault().getLanguage()); - call.resolve(ret); - } - @PluginMethod public void toggleEdgeGestureHandler(PluginCall call) { - Boolean enabled = call.getBoolean("enabled", false); - edgeGestureHandlerEnabled = enabled; + if (getActivity() == null) { + call.reject("activity is null"); + return; + } + + edgeGestureHandlerEnabled = call.getBoolean("enabled", false); + applyBackButtonHandlerState(); if (edgeGestureHandlerEnabled) { - this.onBackPressedCallback.setEnabled(false); setupBackGestureHandlers(); } else { - this.onBackPressedCallback.setEnabled(this.backButtonHandlerEnabled); teardownBackGestureHandlers(); } call.resolve(); } + private void applyBackButtonHandlerState() { + if (this.onBackPressedCallback != null) { + this.onBackPressedCallback.setEnabled(backButtonHandlerEnabled && !edgeGestureHandlerEnabled); + } + } + /** * Handle ACTION_VIEW intents to store a URL that was used to open the app * @param intent @@ -213,6 +223,7 @@ protected void handleOnResume() { @Override protected void handleOnDestroy() { unsetAppListeners(); + teardownBackGestureHandlers(); } private void unsetAppListeners() { @@ -222,6 +233,10 @@ private void unsetAppListeners() { private void setupBackGestureHandlers() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (this.onBackAnimationCallback != null) { + return; + } + this.onBackAnimationCallback = new OnBackAnimationCallback() { @Override public void onBackInvoked() { @@ -245,7 +260,6 @@ public void onBackInvoked() { @Override public void onBackStarted(@NonNull BackEvent backEvent) { if (hasListeners(EVENT_BACK_GESTURE)) { - OnBackAnimationCallback.super.onBackStarted(backEvent); JSObject data = new JSObject(); data.put("phase", "start"); data.put("progress", backEvent.getProgress()); @@ -265,7 +279,6 @@ public void onBackStarted(@NonNull BackEvent backEvent) { @Override public void onBackProgressed(@NonNull BackEvent backEvent) { if (hasListeners(EVENT_BACK_GESTURE)) { - OnBackAnimationCallback.super.onBackStarted(backEvent); JSObject data = new JSObject(); data.put("phase", "progress"); data.put("progress", backEvent.getProgress()); @@ -285,8 +298,6 @@ public void onBackProgressed(@NonNull BackEvent backEvent) { @Override public void onBackCancelled() { if (hasListeners(EVENT_BACK_GESTURE)) { - OnBackAnimationCallback.super.onBackCancelled(); - JSObject data = new JSObject(); data.put("phase", "cancel"); data.put("progress", lastEdgeProgress); @@ -312,6 +323,10 @@ public void onBackCancelled() { private void teardownBackGestureHandlers() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (this.onBackAnimationCallback == null) { + return; + } + getActivity().getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(this.onBackAnimationCallback); this.onBackAnimationCallback = null; } @@ -321,7 +336,6 @@ private String getSwipeEdge(int edge) { return switch (edge) { case EDGE_LEFT -> "left"; case EDGE_RIGHT -> "right"; - case EDGE_NONE -> "none"; default -> "none"; }; } diff --git a/app/src/definitions.ts b/app/src/definitions.ts index 40621219c..2ca96536e 100644 --- a/app/src/definitions.ts +++ b/app/src/definitions.ts @@ -26,7 +26,10 @@ declare module '@capacitor/cli' { * `backButton` handler for the duration that the edge gesture handler * is active. The Android predictive-back integration requires API 34 * (Android 14) or later; on earlier versions the configuration is - * accepted but no events will be emitted. + * accepted but no events will be emitted. Android predictive back + * also requires `android:enableOnBackInvokedCallback="true"` on + * `` in your `AndroidManifest.xml` (on Android 14; + * default on Android 15+). * * @since 9.0.0 * @default false @@ -323,7 +326,10 @@ export interface AppPlugin { * the default `backButton` handler; disabling it restores the previous * back button handler state. The Android predictive-back integration * requires API 34 (Android 14) or later; on earlier versions the call - * resolves but no events will be emitted. + * resolves but no events will be emitted. Android predictive back also + * requires `android:enableOnBackInvokedCallback="true"` on + * `` in your `AndroidManifest.xml` (on Android 14; default + * on Android 15+). * * @since 9.0.0 */ From a2d25599c5d3924b36572cd8e0131c220518feda Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Thu, 11 Jun 2026 12:08:26 -0500 Subject: [PATCH 20/21] refinements for iOS --- app/ios/Sources/AppPlugin/AppPlugin.swift | 47 +++++++++-------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/app/ios/Sources/AppPlugin/AppPlugin.swift b/app/ios/Sources/AppPlugin/AppPlugin.swift index 4c54987c3..ddf6faf6a 100644 --- a/app/ios/Sources/AppPlugin/AppPlugin.swift +++ b/app/ios/Sources/AppPlugin/AppPlugin.swift @@ -21,7 +21,9 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { override public func load() { if getConfig().getBoolean("enableEdgeGestureHandler", false) { - loadGestureRecognizers() + DispatchQueue.main.async { [weak self] in + self?.loadGestureRecognizers() + } } NotificationCenter.default.addObserver(self, selector: #selector(self.handleUrlOpened(notification:)), name: Notification.Name.capacitorOpenURL, object: nil) @@ -148,7 +150,7 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { @objc func handleEdgePan(_ recognizer: UIScreenEdgePanGestureRecognizer) { guard let view = bridge?.webView else { return } - let translation = recognizer.translation(in: webView) + let translation = recognizer.translation(in: view) let touch = recognizer.location(in: view) let viewWidth = view.bounds.width let viewHeight = view.bounds.height @@ -163,21 +165,10 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { let progress = translation.x / viewWidth data["swipeEdge"] = "left" data["progress"] = max(0, min(1, progress)) - case .right: let progress = -translation.x / viewWidth data["swipeEdge"] = "right" data["progress"] = max(0, min(1, progress)) - case .top: - let progress = translation.y / viewHeight - data["swipeEdge"] = "top" - data["progress"] = max(0, min(1, progress)) - case .bottom: - let progress = -translation.y / viewHeight - data["swipeEdge"] = "bottom" - data["progress"] = max(0, min(1, progress)) - case .all: - break default: break } @@ -199,11 +190,17 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { break } - notifyListeners("edgeGesture", data: data) + // dont notify if there is no phase + guard data["phase"] != nil else { return } + + if hasListeners("edgeGesture") { + notifyListeners("edgeGesture", data: data) + } } private func loadGestureRecognizers() { - guard let window = UIApplication.shared.delegate?.window ?? nil else { return } + guard self.leftEdgePanRecognizer == nil, self.rightEdgePanRecognizer == nil else { return } + guard let window = bridge?.viewController?.view.window else { return } let leftEdgePanRecognizer = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(self.handleEdgePan(_:))) leftEdgePanRecognizer.delegate = self @@ -221,26 +218,20 @@ public class AppPlugin: CAPPlugin, CAPBridgedPlugin { } private func destroyGestureRecognizers() { - guard let window = UIApplication.shared.delegate?.window ?? nil, - let leftEdgePanRecognizer = self.leftEdgePanRecognizer, + guard let leftEdgePanRecognizer = self.leftEdgePanRecognizer, let rightEdgePanRecognizer = self.rightEdgePanRecognizer else { return } - window.removeGestureRecognizer(leftEdgePanRecognizer) - window.removeGestureRecognizer(rightEdgePanRecognizer) + leftEdgePanRecognizer.view?.removeGestureRecognizer(leftEdgePanRecognizer) + rightEdgePanRecognizer.view?.removeGestureRecognizer(rightEdgePanRecognizer) + + self.leftEdgePanRecognizer = nil + self.rightEdgePanRecognizer = nil } } extension AppPlugin: UIGestureRecognizerDelegate { public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false - } - - public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool { - return false + return gestureRecognizer === leftEdgePanRecognizer || gestureRecognizer === rightEdgePanRecognizer } } From 3c03ab9f13b56826431869c8de3887e8f4989821 Mon Sep 17 00:00:00 2001 From: Joseph Pender Date: Thu, 11 Jun 2026 12:28:37 -0500 Subject: [PATCH 21/21] unsupported methods on web --- app/src/web.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/web.ts b/app/src/web.ts index ca68695f9..b749bee16 100644 --- a/app/src/web.ts +++ b/app/src/web.ts @@ -33,7 +33,7 @@ export class AppWeb extends WebPlugin implements AppPlugin { } async toggleEdgeGestureHandler(): Promise { - throw new Error('Method not implemented.'); + throw this.unimplemented('Not implemented on web.'); } private handleVisibilityChange = () => {