Describe the problem
The stripe_android Flutter plugin (tested with v12.1.0 and v10.0.0) ships ~30 stub source files under com.facebook.react.* in its android/src/main/kotlin/com/facebook/ directory (e.g. UiThreadUtil.kt, MapBuilder.kt, ReactContext.kt, ReactApplicationContext.java, Arguments.java, ReadableMap.java, etc.).
These stubs are minimal shims used internally by Stripe's React Native SDK bridge code (com.reactnativestripesdk.*). However, they have incompatible method signatures compared to the real React Native library (com.facebook.react:react-android).
When another SDK in the same app depends on the real React Native (e.g. CometChat Calls SDK depends on react-android:0.77.2), the duplicate classes cause fatal crashes at runtime because the dex merger silently picks one version, and whichever loses is broken:
- If Stripe's stubs win → CometChat crashes with
NoSuchMethodError (e.g. UiThreadUtil.runOnUiThread(Runnable)Z or MapBuilder.builder())
- If real RN wins → Stripe crashes with
PlatformException: Stripe SDK did not initialize (because the real ReactApplicationContext has different constructors than Stripe's stub)
What was the expected behavior?
The stripe_android plugin should not ship classes under the com.facebook.react package namespace, as this conflicts with any app that also uses the real React Native library. The stubs should either:
- Be relocated to a Stripe-specific package (e.g.
com.stripe.compat.react.*)
- Use
compileOnly dependencies on the real React Native artifact instead of stub source files
- Be isolated in some other way that prevents classpath collisions
Reproduction
- Step 1: Create a Flutter app with both
flutter_stripe: ^12.1.0 and cometchat_calls_sdk: ^4.2.1 (or any SDK that depends on com.facebook.react:react-android)
- Step 2: Build and run on Android (debug or release)
- Step 3: Trigger CometChat call initialization → app crashes
Crash 1 – Stripe's UiThreadUtil.kt stub wins the dex merge (no @JvmStatic bridge methods):
java.lang.NoSuchMethodError: No static method runOnUiThread(Ljava/lang/Runnable;)Z
in class Lcom/facebook/react/bridge/UiThreadUtil;
at com.facebook.react.modules.core.ReactChoreographer.<init>(ReactChoreographer.kt:71)
at com.facebook.react.ReactInstanceManager.<init>(ReactInstanceManager.java:315)
at com.cometchat.calls.helpers.RNHelper.initReactInstanceManager(RNHelper.java:247)
Crash 2 – Stripe's MapBuilder.kt stub wins (no builder() method):
java.lang.NoSuchMethodError: No static method builder()Lcom/facebook/react/common/MapBuilder$Builder;
in class Lcom/facebook/react/common/MapBuilder;
at com.facebook.react.ReactAndroidHWInputDeviceHelper.<clinit>(ReactAndroidHWInputDeviceHelper.java:25)
at com.facebook.react.ReactRootView.<init>(ReactRootView.java:108)
at com.cometchat.calls.helpers.RNHelper.getView(RNHelper.java:269)
Root cause confirmed by inspecting the APK:
react-android:0.77.2 JAR contains UiThreadUtil.class compiled from Java with public static boolean runOnUiThread(Runnable) (descriptor (Ljava/lang/Runnable;)Z)
- The APK's
classes2.dex instead contains Stripe's Kotlin stub UiThreadUtil which has only a Companion field and access$getHandler$cp() — no static runOnUiThread method at all
Workaround: Use ASM ClassRemapper in a Gradle doLast hook to relocate all com/facebook/** classes in stripe_android's output JARs to com/stripe/compat/facebook/**, so both Stripe's stubs and the real React Native can coexist:
// In android/build.gradle (root)
import org.objectweb.asm.*
import org.objectweb.asm.commons.*
def relocateReactClassesInJar(File jarFile) {
def OLD_PREFIX = 'com/facebook/'
def NEW_PREFIX = 'com/stripe/compat/facebook/'
def remapper = new Remapper() {
@Override String map(String internalName) {
if (internalName.startsWith(OLD_PREFIX)) {
return NEW_PREFIX + internalName.substring(OLD_PREFIX.length())
}
return internalName
}
}
def tmpFile = new File(jarFile.parentFile, jarFile.name + '.tmp')
new java.util.zip.ZipOutputStream(tmpFile.newOutputStream()).withCloseable { zos ->
new java.util.zip.ZipFile(jarFile).withCloseable { zin ->
def entries = zin.entries()
while (entries.hasMoreElements()) {
def entry = entries.nextElement()
if (entry.directory) {
def dirName = entry.name.startsWith(OLD_PREFIX) ?
NEW_PREFIX + entry.name.substring(OLD_PREFIX.length()) : entry.name
zos.putNextEntry(new java.util.zip.ZipEntry(dirName))
zos.closeEntry()
} else if (entry.name.endsWith('.class')) {
def bytes = zin.getInputStream(entry).bytes
def reader = new ClassReader(bytes)
def writer = new ClassWriter(0)
reader.accept(new ClassRemapper(writer, remapper), 0)
def newName = entry.name.startsWith(OLD_PREFIX) ?
NEW_PREFIX + entry.name.substring(OLD_PREFIX.length()) : entry.name
zos.putNextEntry(new java.util.zip.ZipEntry(newName))
zos.write(writer.toByteArray())
zos.closeEntry()
} else {
zos.putNextEntry(new java.util.zip.ZipEntry(entry.name))
zin.getInputStream(entry).withCloseable { is -> zos << is }
zos.closeEntry()
}
}
}
}
jarFile.delete()
tmpFile.renameTo(jarFile)
}
subprojects { sub ->
if (sub.name == 'stripe_android') {
sub.afterEvaluate {
sub.tasks.matching {
it.name.contains('bundleLibRuntimeToJar') ||
it.name.contains('bundleLibCompileToJar')
}.configureEach { task ->
task.doLast {
task.outputs.files.each { file ->
if (file.name.endsWith('.jar') && file.exists()) {
relocateReactClassesInJar(file)
}
}
}
}
}
}
}
Environment
- Version used:
flutter_stripe: 12.1.0 / stripe_android: 12.1.0 (also reproduced with stripe_android: 10.0.0)
- Other modules/plugins/libraries that might be involved:
cometchat_calls_sdk: 4.2.1 (depends on com.cometchat:calls-sdk-android:4.3.3 which depends on com.facebook.react:react-android:0.77.2)
- Flutter: 3.35.0+, Dart 3.9.0+
- AGP: 8.1+
- Android: debug and release builds affected
Describe the problem
The
stripe_androidFlutter plugin (tested with v12.1.0 and v10.0.0) ships ~30 stub source files undercom.facebook.react.*in itsandroid/src/main/kotlin/com/facebook/directory (e.g.UiThreadUtil.kt,MapBuilder.kt,ReactContext.kt,ReactApplicationContext.java,Arguments.java,ReadableMap.java, etc.).These stubs are minimal shims used internally by Stripe's React Native SDK bridge code (
com.reactnativestripesdk.*). However, they have incompatible method signatures compared to the real React Native library (com.facebook.react:react-android).When another SDK in the same app depends on the real React Native (e.g. CometChat Calls SDK depends on
react-android:0.77.2), the duplicate classes cause fatal crashes at runtime because the dex merger silently picks one version, and whichever loses is broken:NoSuchMethodError(e.g.UiThreadUtil.runOnUiThread(Runnable)ZorMapBuilder.builder())PlatformException: Stripe SDK did not initialize(because the realReactApplicationContexthas different constructors than Stripe's stub)What was the expected behavior?
The
stripe_androidplugin should not ship classes under thecom.facebook.reactpackage namespace, as this conflicts with any app that also uses the real React Native library. The stubs should either:com.stripe.compat.react.*)compileOnlydependencies on the real React Native artifact instead of stub source filesReproduction
flutter_stripe: ^12.1.0andcometchat_calls_sdk: ^4.2.1(or any SDK that depends oncom.facebook.react:react-android)Crash 1 – Stripe's
UiThreadUtil.ktstub wins the dex merge (no@JvmStaticbridge methods):Crash 2 – Stripe's
MapBuilder.ktstub wins (nobuilder()method):Root cause confirmed by inspecting the APK:
react-android:0.77.2JAR containsUiThreadUtil.classcompiled from Java withpublic static boolean runOnUiThread(Runnable)(descriptor(Ljava/lang/Runnable;)Z)classes2.dexinstead contains Stripe's Kotlin stubUiThreadUtilwhich has only aCompanionfield andaccess$getHandler$cp()— no staticrunOnUiThreadmethod at allWorkaround: Use ASM
ClassRemapperin a GradledoLasthook to relocate allcom/facebook/**classes instripe_android's output JARs tocom/stripe/compat/facebook/**, so both Stripe's stubs and the real React Native can coexist:Environment
flutter_stripe: 12.1.0/stripe_android: 12.1.0(also reproduced withstripe_android: 10.0.0)cometchat_calls_sdk: 4.2.1(depends oncom.cometchat:calls-sdk-android:4.3.3which depends oncom.facebook.react:react-android:0.77.2)