104 lines · 4.0 KB · JavaScript
RawDownload
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;  });};