const { withMainActivity } = require("expo/config-plugins");// predictiveBackGestureEnabled writes android:enableOnBackInvokedCallback="true",// which retires the legacy KEYCODE_BACK/onBackPressed() delivery on Android 13+.// From then on back gestures only reach the app through OnBackPressedDispatcher// callbacks. react-native 0.85 registers its own always-enabled callback, but// only on Android 16 with targetSdk 36 (ReactActivity's enforced-predictive-back// workaround) — on Android 13-15 nothing is registered, the system consumes// every back gesture itself, and JS back handling (React Navigation pops,// BackHandler listeners) silently dies: each gesture just backgrounds the app.// This plugin mirrors react-native's shim on API 33-35 so back keeps flowing// to JS there. See https://github.com/software-mansion/react-native-screens/discussions/2540.const CALLBACK_PROPERTY = ` // Routes predictive-back gestures to JS on Android 13-15, where react-native // registers no OnBackPressedDispatcher callback of its own (it only does on // Android 16+). Registered in onCreate; added by withAndroidPredictiveBackCompat. private val predictiveBackCompatCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { isEnabled = false onBackPressed() isEnabled = true } }`;const CALLBACK_REGISTRATION = ` if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) { onBackPressedDispatcher.addCallback(this, predictiveBackCompatCallback) }`;// Wraps the template's invokeDefaultOnBackPressed: the default action ends in// ComponentActivity.onBackPressed(), which re-enters the dispatcher — with the// compat callback still enabled that would bounce back into JS forever instead// of backgrounding the app.const INVOKE_DEFAULT_WRAPPER = `override fun invokeDefaultOnBackPressed() { predictiveBackCompatCallback.isEnabled = false try { invokeDefaultOnBackPressedLegacy() } finally { predictiveBackCompatCallback.isEnabled = true } } private fun invokeDefaultOnBackPressedLegacy() {`;function insertAfter(contents, anchor, insertion, description) { const index = contents.indexOf(anchor); if (index === -1) { throw new Error( `withAndroidPredictiveBackCompat: could not find ${description} in MainActivity — the Expo template changed; update the plugin anchors.`, ); } const end = index + anchor.length; return contents.slice(0, end) + insertion + contents.slice(end);}module.exports = function withAndroidPredictiveBackCompat(config) { if (config.android?.predictiveBackGestureEnabled !== true) { return config; } return withMainActivity(config, (nextConfig) => { let contents = nextConfig.modResults.contents; if (nextConfig.modResults.language !== "kt") { throw new Error("withAndroidPredictiveBackCompat: MainActivity must be Kotlin."); } if (contents.includes("predictiveBackCompatCallback")) { return nextConfig; } contents = insertAfter( contents, "import android.os.Bundle", "\nimport androidx.activity.OnBackPressedCallback", "the android.os.Bundle import", ); contents = insertAfter( contents, "class MainActivity : ReactActivity() {", CALLBACK_PROPERTY, "the MainActivity class declaration", ); contents = insertAfter( contents, "super.onCreate(null)", CALLBACK_REGISTRATION, "the super.onCreate call", ); if (!contents.includes("override fun invokeDefaultOnBackPressed() {")) { throw new Error( "withAndroidPredictiveBackCompat: could not find invokeDefaultOnBackPressed in MainActivity — the Expo template changed; update the plugin anchors.", ); } contents = contents.replace( "override fun invokeDefaultOnBackPressed() {", INVOKE_DEFAULT_WRAPPER, ); nextConfig.modResults.contents = contents; return nextConfig; });};
const { withMainActivity } = require("expo/config-plugins");
// predictiveBackGestureEnabled writes android:enableOnBackInvokedCallback="true",
// which retires the legacy KEYCODE_BACK/onBackPressed() delivery on Android 13+.
// From then on back gestures only reach the app through OnBackPressedDispatcher
// callbacks. react-native 0.85 registers its own always-enabled callback, but
// only on Android 16 with targetSdk 36 (ReactActivity's enforced-predictive-back
// workaround) — on Android 13-15 nothing is registered, the system consumes
// every back gesture itself, and JS back handling (React Navigation pops,
// BackHandler listeners) silently dies: each gesture just backgrounds the app.
// This plugin mirrors react-native's shim on API 33-35 so back keeps flowing
// to JS there. See https://github.com/software-mansion/react-native-screens/discussions/2540.
const CALLBACK_PROPERTY = `
// Routes predictive-back gestures to JS on Android 13-15, where react-native
// registers no OnBackPressedDispatcher callback of its own (it only does on
// Android 16+). Registered in onCreate; added by withAndroidPredictiveBackCompat.
private val predictiveBackCompatCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
isEnabled = false
onBackPressed()
isEnabled = true
}
}
`;
const CALLBACK_REGISTRATION = `
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) {
onBackPressedDispatcher.addCallback(this, predictiveBackCompatCallback)
}`;
// Wraps the template's invokeDefaultOnBackPressed: the default action ends in
// ComponentActivity.onBackPressed(), which re-enters the dispatcher — with the
// compat callback still enabled that would bounce back into JS forever instead
// of backgrounding the app.
const INVOKE_DEFAULT_WRAPPER = `override fun invokeDefaultOnBackPressed() {
predictiveBackCompatCallback.isEnabled = false
try {
invokeDefaultOnBackPressedLegacy()
} finally {
predictiveBackCompatCallback.isEnabled = true
}
}
private fun invokeDefaultOnBackPressedLegacy() {`;
function insertAfter(contents, anchor, insertion, description) {
const index = contents.indexOf(anchor);
if (index === -1) {
throw new Error(
`withAndroidPredictiveBackCompat: could not find ${description} in MainActivity — the Expo template changed; update the plugin anchors.`,
);
}
const end = index + anchor.length;
return contents.slice(0, end) + insertion + contents.slice(end);
}
module.exports = function withAndroidPredictiveBackCompat(config) {
if (config.android?.predictiveBackGestureEnabled !== true) {
return config;
}
return withMainActivity(config, (nextConfig) => {
let contents = nextConfig.modResults.contents;
if (nextConfig.modResults.language !== "kt") {
throw new Error("withAndroidPredictiveBackCompat: MainActivity must be Kotlin.");
}
if (contents.includes("predictiveBackCompatCallback")) {
return nextConfig;
}
contents = insertAfter(
contents,
"import android.os.Bundle",
"\nimport androidx.activity.OnBackPressedCallback",
"the android.os.Bundle import",
);
contents = insertAfter(
contents,
"class MainActivity : ReactActivity() {",
CALLBACK_PROPERTY,
"the MainActivity class declaration",
);
contents = insertAfter(
contents,
"super.onCreate(null)",
CALLBACK_REGISTRATION,
"the super.onCreate call",
);
if (!contents.includes("override fun invokeDefaultOnBackPressed() {")) {
throw new Error(
"withAndroidPredictiveBackCompat: could not find invokeDefaultOnBackPressed in MainActivity — the Expo template changed; update the plugin anchors.",
);
}
contents = contents.replace(
"override fun invokeDefaultOnBackPressed() {",
INVOKE_DEFAULT_WRAPPER,
);
nextConfig.modResults.contents = contents;
return nextConfig;
});
};