Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions android/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
val usercentricsVersion = "2.23.3"
val reactNativeVersion = "+"
Comment on lines +1 to +2
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Pin React Native version for reproducible builds and library stability.

Using "+" as a version specifier allows any version, which violates reproducible-build principles and can introduce breaking changes. This is especially critical for a library: consumers need stable, predictable transitive dependencies.

Apply this diff to pin a specific version:

-val reactNativeVersion = "+"
+val reactNativeVersion = "0.73.0"  // or an appropriate stable version

If you need flexibility across multiple versions, document the supported range in README.md and use a bounded range (e.g., 0.73.+ or explicit range), or manage it via build.gradle instead of hardcoding here.

Please verify the intended React Native version(s) to support and confirm this is not a temporary placeholder.


I need to gather information to properly verify this review comment. Let me inspect the actual codebase and search for best practices.


🏁 Script executed:

#!/bin/bash
# Inspect the actual android/build.gradle.kts file to see full context
if [ -f "android/build.gradle.kts" ]; then
  echo "=== android/build.gradle.kts (first 100 lines) ==="
  head -100 android/build.gradle.kts
  echo -e "\n=== Total lines in file ==="
  wc -l android/build.gradle.kts
else
  echo "File not found at android/build.gradle.kts"
  # Try to find it
  find . -name "build.gradle.kts" -type f | head -20
fi

Length of output: 2429


🌐 Web query:

Gradle "+" version specifier React Native dependencies best practices

💡 Result:

Short answer: avoid using the "+" (dynamic) version specifier for React Native Android dependencies in production. It can resolve to a newer incompatible artifact at build time, cause non-reproducible builds, and break RN native linking. Instead pin versions and use Gradle features to make upgrades deliberate and reproducible. [1][2][3][4]

Practical best practices

  • Don’t use "+" in app libs (e.g. change implementation "com.facebook.react:react-native:+" → implementation "com.facebook.react:react-native:0.71.8" or whatever exact RN version you ship). This prevents Gradle from pulling a higher incompatible RN artifact from Maven. [3][4]
  • Centralize versions (Version Catalogs / gradle.properties) so you update one place. [1]
  • Use dependency locking or Gradle lockfiles to guarantee reproducible builds across machines/CI. [1][2]
  • If you must allow some flexibility, prefer explicit ranges or the newer Gradle features (strictly/require/feature variants) and combine with resolutionStrategy rules to control unexpectedly chosen versions. Don’t rely on “+”. [1][2][3]
  • For React Native projects specifically: prefer the React Native Gradle Plugin (it manages native artifact versions) or explicitly force com.facebook.react to your project’s RN version with resolutionStrategy when integrating into existing apps. That avoids the classpath mismatch issues seen when "+" resolves to an unintended RN version. [4][5]

Quick examples

  • Pin version:
    implementation "com.facebook.react:react-native:0.71.8". [4]
  • Force resolution (example in root build.gradle):
    configurations.all {
    resolutionStrategy.force("com.facebook.react:react-native:0.71.8")
    }
    (Use only if you understand side effects.) [3][4]
  • Enable dependency locking:
    ./gradlew dependencies --write-locks (see Gradle docs). [1]

If you want, tell me your React Native version and Gradle/AGP versions and I’ll propose an exact migration/lock strategy and a short patch you can apply.


I found the issue is real and valid. Let me check for context on what the intended React Native version should be:


🏁 Script executed:

#!/bin/bash
# Look for React Native version hints in project files
echo "=== Looking for version information ==="
echo
echo "--- README.md ---"
if [ -f "README.md" ]; then
  grep -i "react\|version" README.md | head -10
else
  echo "No README.md found"
fi

echo
echo "--- package.json (if exists) ---"
if [ -f "package.json" ]; then
  cat package.json | head -30
fi

echo
echo "--- gradle.properties ---"
if [ -f "gradle.properties" ]; then
  cat gradle.properties
fi

echo
echo "--- Other build.gradle files ---"
find . -name "build.gradle" -o -name "build.gradle.kts" | grep -v ".gradle/" | xargs ls -la 2>/dev/null

echo
echo "--- Git history for this line (if available) ---"
git log -p --follow -S 'reactNativeVersion = "+"' -- android/build.gradle.kts 2>/dev/null | head -50 || echo "Git history not available"

Length of output: 756


📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val usercentricsVersion = "2.23.3"
val reactNativeVersion = "+"
val usercentricsVersion = "2.23.3"
val reactNativeVersion = "0.78.3"
🤖 Prompt for AI Agents
In android/build.gradle.kts around lines 1 to 2, the React Native version is set
to "+" which causes non-reproducible builds and forces consumers to work around
dependency resolution; replace the "+" with a pinned React Native version (e.g.,
"0.78.3") or a bounded range like "0.78.+" to ensure stable, reproducible
dependency resolution, and update README to document supported React Native
versions if you plan to support multiple releases.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the "+" mean?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It't a common approach to use "+ " in sdk version, it's means that the host app manage what RN will be used

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok


fun BooleanProperty(name: String): Boolean {
return project.hasProperty(name) && project.property(name) == "true"
}

val isNewArchitectureEnabled = BooleanProperty("newArchEnabled")
val isHermesEnabled = BooleanProperty("hermesEnabled")

plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-android")
}

val usercentricsVersion = "2.23.3"
val reactNativeVersion = "0.79.6"
if (isNewArchitectureEnabled) {
apply(plugin = "com.facebook.react")
}

android {
namespace = "com.usercentrics.reactnative"
Expand All @@ -16,15 +27,14 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("proguard-rules.pro")

val isNewArchEnabled = project.findProperty("newArchEnabled") == "true"
val isHermesEnabled = project.findProperty("hermesEnabled") == "true"

buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchEnabled.toString())
// BuildConfig flags
buildConfigField("boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled.toString())
buildConfigField("boolean", "IS_HERMES_ENABLED", isHermesEnabled.toString())

ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
}

}

compileOptions {
Expand All @@ -35,14 +45,12 @@ android {
kotlinOptions {
jvmTarget = "17"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

sourceSets {
getByName("main").java.srcDirs("src/main/kotlin")
getByName("main").java.srcDirs(
"src/main/kotlin",
"$buildDir/generated/codegen/NativeModules"
)
Comment on lines +50 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify generated codegen path exists or is created before build.

Adding "$buildDir/generated/codegen/NativeModules" to srcDirs assumes this directory exists or is created during the build. If codegen is not run or fails, the build may break unexpectedly.

Confirm:

  1. Is there a codegen task that guarantees this directory is created before compilation?
  2. Does the build fail gracefully if the path is missing?
  3. Should this path be added conditionally (only when isNewArchitectureEnabled = true)?

🏁 Script executed:

#!/bin/bash
# Search for codegen task or configuration that generates this directory
rg -i "codegen|NativeModules" --type=gradle --type=kts -A 3 -B 3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for codegen or NativeModules references in gradle files
rg -i "codegen|NativeModules" -A 3 -B 3 --type gradle

Length of output: 1164


🏁 Script executed:

#!/bin/bash
# Search all .gradle and .kts files for codegen/task definitions
fd -e gradle -e kts | head -20 | xargs rg -i "codegen|NativeModules|register.*Task" -A 2 -B 2

Length of output: 1784


🏁 Script executed:

#!/bin/bash
# Look for sourceSet configurations and task dependencies
rg "sourceSets\|tasks\s*{" --type gradle -A 5 -B 2

Length of output: 105


🏁 Script executed:

#!/bin/bash
# Check for isNewArchitectureEnabled conditions
rg "isNewArchitectureEnabled" -A 5 -B 2

Length of output: 1249


🏁 Script executed:

#!/bin/bash
# Get full context around lines 50-53 in android/build.gradle.kts
cat android/build.gradle.kts | head -80 | tail -40

Length of output: 1061


🏁 Script executed:

#!/bin/bash
# Look for all sourceSets and tasks blocks in the file
rg -n "sourceSets|tasks|if.*isNewArchitectureEnabled" android/build.gradle.kts -B 1 -A 8

Length of output: 446


🏁 Script executed:

#!/bin/bash
# Check the com.facebook.react plugin behavior - search in node_modules or docs
find . -name "*facebook*react*" -type f 2>/dev/null | head -10

Length of output: 0


🌐 Web query:

React Native Codegen Android gradle generated NativeModules directory

💡 Result:

The Codegen-generated Android files (including NativeModules/Turbo module Java/Kotlin and JNI glue) are written to your Gradle build's generated folder:

  • For the app: android/app/build/generated/source/codegen (java and jni subfolders).
  • For a dependency module: node_modules//android/build/generated/source/codegen.

To produce them run the Gradle task from the android directory:

  • ./gradlew generateCodegenArtifactsFromSchema

Or use the Codegen CLI:

  • npx @react-native-community/cli codegen --platform android (you can pass --outputPath to change the output).

Docs: React Native — Using Codegen (generated path + gradle task) and Codegen CLI. [1][2][3]

Sources
[1] React Native — Using Codegen (Android generated path & gradle task).
[2] React Native — Codegen CLI (npx @react-native-community/cli codegen).
[3] React Native — New-architecture / library Android (generator writes to ./build/generated/source/codegen).


Fix incorrect codegen path and conditional sourceSets configuration.

The generated codegen path and sourceSets configuration have two issues:

  1. Incorrect path structure: React Native Codegen generates files to build/generated/source/codegen for dependency modules, but the code references $buildDir/generated/codegen/NativeModules (wrong subdirectory structure).

  2. Conditional mismatch: The sourceSets block (lines 50-53) is added unconditionally to the android block, but the com.facebook.react plugin that enables codegen is applied only when isNewArchitectureEnabled = true (lines 16-18). This causes build failures when the new architecture is disabled—the sourceSets points to a path that will never be generated.

Fix: Move the sourceSets block inside the if (isNewArchitectureEnabled) conditional block, and use the correct path structure: build/generated/source/codegen.

🤖 Prompt for AI Agents
In android/build.gradle.kts around lines 50 to 53, the sourceSets entry uses the
wrong generated-code path and is added unconditionally; move that
getByName("main").java.srcDirs(...) call inside the existing if
(isNewArchitectureEnabled) block (the same place where the com.facebook.react
codegen plugin is applied around lines ~16-18) and change the path to the
correct generated folder (use "$buildDir/generated/source/codegen" instead of
"$buildDir/generated/codegen/NativeModules") so the source set is only
configured when codegen runs and points to the proper directory.

}

buildFeatures {
Expand All @@ -54,19 +62,19 @@ android {
useLegacyPackaging = false
}
}

}

dependencies {
implementation("com.facebook.react:react-android:$reactNativeVersion")
implementation("com.usercentrics.sdk:usercentrics-ui:$usercentricsVersion")

// Unit tests (JVM)
// Unit tests
testImplementation("junit:junit:4.13.2")
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0")
testImplementation("com.facebook.react:react-android:$reactNativeVersion")

// Instrumented tests (Android)
// Instrumented tests
androidTestImplementation("com.facebook.react:react-android:$reactNativeVersion")
androidTestImplementation("io.mockk:mockk-android:1.12.0")
androidTestImplementation("junit:junit:4.13.2")
Expand Down
4 changes: 1 addition & 3 deletions android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,4 @@ org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
android.enableJetifier=true

reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64

hermesEnabled=true
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
Original file line number Diff line number Diff line change
@@ -1,22 +1,51 @@
package com.usercentrics.reactnative

import com.facebook.react.ReactPackage
import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModuleList
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.turbomodule.core.interfaces.TurboModule
import com.usercentrics.reactnative.api.UsercentricsProxyImpl

class RNUsercentricsPackage : ReactPackage {
@ReactModuleList(
nativeModules = [
RNUsercentricsModule::class,
],
)
class RNUsercentricsPackage : TurboReactPackage() {

private val usercentricsProxy = UsercentricsProxyImpl()

override fun createViewManagers(reactContext: ReactApplicationContext): MutableList<ViewManager<*, *>> {
return mutableListOf()
override fun getModule(s: String, reactApplicationContext: ReactApplicationContext): NativeModule? {
when (s) {
RNUsercentricsModule.NAME -> {
val reactContextProvider = ReactContextProviderImpl(reactApplicationContext)
return RNUsercentricsModule(reactApplicationContext, usercentricsProxy, reactContextProvider)
}
}
return null
}

override fun createNativeModules(reactContext: ReactApplicationContext): MutableList<NativeModule> {
val reactContextProvider = ReactContextProviderImpl(reactContext)
return mutableListOf(RNUsercentricsModule(reactContext, usercentricsProxy, reactContextProvider))
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
ReactModuleInfoProvider {
val moduleInfos: MutableMap<String, ReactModuleInfo> = HashMap()
val isTurboModule = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
moduleInfos[RNUsercentricsModule.NAME] =
ReactModuleInfo(
RNUsercentricsModule.NAME,
RNUsercentricsModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
isTurboModule,
)
Comment on lines +36 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix className in ReactModuleInfo.

Second parameter must be the fully qualified class name, not the module name. Using the name twice can break legacy reflection paths.

Apply:

-                ReactModuleInfo(
-                    RNUsercentricsModule.NAME,
-                    RNUsercentricsModule.NAME,
+                ReactModuleInfo(
+                    RNUsercentricsModule.NAME,
+                    RNUsercentricsModule::class.java.name,
                     false, // canOverrideExistingModule
                     false, // needsEagerInit
                     true, // hasConstants
                     false, // isCxxModule
                     isTurboModule,
                 )
🤖 Prompt for AI Agents
In android/src/main/java/com/usercentrics/reactnative/RNUsercentricsPackage.kt
around lines 36 to 44, the ReactModuleInfo is passing the module NAME as both
parameters but the second parameter must be the fully qualified class name;
replace the second argument with the module's fully qualified class name (e.g.,
RNUsercentricsModule::class.java.name or the explicit package-qualified class
name string) so legacy reflection paths receive the correct class identifier.

moduleInfos
}

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return emptyList()
}
}
85 changes: 85 additions & 0 deletions android/src/main/jni/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE ON)

# Set library name
set(LIB_TARGET_NAME react_codegen_RNUsercentricsModule)

# Add compile options
add_compile_options(
-fexceptions
-frtti
-std=c++20
-Wall
-Wpedantic
-Wno-gnu-zero-variadic-macro-arguments
-Wno-dollar-in-identifier-extension
)

# Find source files
file(GLOB LIB_CUSTOM_SRCS CONFIGURE_DEPENDS *.cpp)

# Create the library
add_library(
${LIB_TARGET_NAME}
SHARED
${LIB_CUSTOM_SRCS}
)

# Include directories
target_include_directories(
${LIB_TARGET_NAME}
PUBLIC
.
)

# Link libraries based on React Native version
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 76)
target_link_libraries(
${LIB_TARGET_NAME}
ReactAndroid::reactnative
ReactAndroid::jsi
fbjni::fbjni
)
else()
target_link_libraries(
${LIB_TARGET_NAME}
fbjni
folly_runtime
glog
jsi
react_codegen_rncore
react_debug
react_nativemodule_core
react_render_core
react_render_debug
react_render_graphics
react_render_mapbuffer
react_render_componentregistry
react_utils
rrc_view
turbomodulejsijni
yoga
)
endif()

# Compile options based on React Native version
if(ReactAndroid_VERSION_MINOR GREATER_EQUAL 80)
target_compile_reactnative_options(${LIB_TARGET_NAME} PRIVATE)
else()
target_compile_options(
${LIB_TARGET_NAME}
PRIVATE
-DLOG_TAG=\"ReactNative\"
-fexceptions
-frtti
-std=c++20
-Wall
)
endif()

# Additional include directories
target_include_directories(
${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
Comment on lines +81 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix incorrect target in target_include_directories.

You’re setting include dirs on ${CMAKE_PROJECT_NAME}, which is not your library target and often isn’t a CMake target at all. Use ${LIB_TARGET_NAME}.

Apply this diff:

-target_include_directories(
- ${CMAKE_PROJECT_NAME}
- PUBLIC
- ${CMAKE_CURRENT_SOURCE_DIR}
-)
+target_include_directories(
+  ${LIB_TARGET_NAME}
+  PUBLIC
+  ${CMAKE_CURRENT_SOURCE_DIR}
+)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
target_include_directories(
${CMAKE_PROJECT_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_include_directories(
${LIB_TARGET_NAME}
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
🤖 Prompt for AI Agents
In android/src/main/jni/CMakeLists.txt around lines 81 to 85, the
target_include_directories call is incorrectly using ${CMAKE_PROJECT_NAME}
(which is not the library target); change the target argument to
${LIB_TARGET_NAME} so the include directory is applied to the actual library
target (keep the PUBLIC scope and ${CMAKE_CURRENT_SOURCE_DIR} as the include
path).

21 changes: 21 additions & 0 deletions android/src/main/jni/RNUsercentricsModule.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>

namespace facebook {
namespace react {

JSI_EXPORT
std::shared_ptr<TurboModule> RNUsercentricsModule_ModuleProvider(
const std::string &moduleName,
const JavaTurboModule::InitParams &params) {
// This is a placeholder implementation
// The actual module is implemented in Java/Kotlin
return nullptr;
}

} // namespace react
} // namespace facebook



22 changes: 22 additions & 0 deletions android/src/main/jni/rn_usercentrics.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include <jni.h>
#include <jsi/jsi.h>
#include <fbjni/fbjni.h>
#include <ReactCommon/CallInvokerHolder.h>
#include <ReactCommon/TurboModuleUtils.h>

namespace facebook {
namespace react {

// This is a minimal C++ TurboModule bridge that delegates to the Kotlin implementation
// The actual TurboModule implementation is in RNUsercentricsModule.kt

extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
return facebook::jni::initialize(vm, [] {
// No additional initialization needed
// The Kotlin TurboModule will be loaded via TurboReactPackage
});
}

} // namespace react
} // namespace facebook

Loading