t3-code-android-nightly/apps/mobile/plugins/withAndroidPredictiveBackCompat.cjs
Julius Marminge c1ec1915fc
[codex] Add Android mobile support (#3579)
Co-authored-by: Horus Lugo <horusgoul@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: maria-rcks <maria@kuuro.net>
Co-authored-by: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com>
Co-authored-by: Ben Davis <45952064+bmdavis419@users.noreply.github.com>
Co-authored-by: Alex <me@pixp.cc>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@mac.lan>
2026-07-12 12:03:35 +02:00

104 lines
4 KiB
JavaScript

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;
});
};