Skip to content

fix(android): allow BuildConfig lookup when applicationId != namespace#70

Draft
airowe wants to merge 2 commits into
frontegg:masterfrom
healthie:fix/android-buildconfig-lookup
Draft

fix(android): allow BuildConfig lookup when applicationId != namespace#70
airowe wants to merge 2 commits into
frontegg:masterfrom
healthie:fix/android-buildconfig-lookup

Conversation

@airowe
Copy link
Copy Markdown

@airowe airowe commented May 28, 2026

Summary

Adds a single-step fallback so Context.fronteggConstants can locate the host app's generated BuildConfig when its applicationId differs from its AGP namespace. Existing apps see no behaviour change.

Problem

// Utils.kt — current
val packageName = this.packageName
val className = "$packageName.BuildConfig"
val buildConfigClass = Class.forName(className)  // ClassNotFoundException

Context.getPackageName() returns the Gradle applicationId at runtime, but AGP emits the generated BuildConfig under the module's namespace. For apps where these differ — common in white-label / multi-flavor RN apps that share a Java source tree but ship under different applicationIds — the SDK throws ClassNotFoundException at module init time before any JS code runs.

In our case (Healthie + 8 white-label flavors): applicationId = com.healthie.app.<flavor>, namespace = com.main, BuildConfig lives at com.main.BuildConfig, lookup fails.

Fix

resolveBuildConfigClass now tries the candidates in order:

  1. <applicationId>.BuildConfig — the existing behaviour. For apps where applicationId == namespace this matches immediately and step 2 never runs.
  2. The value of a <meta-data> entry on <application> named com.frontegg.reactnative.BUILD_CONFIG_PACKAGE, suffixed with .BuildConfig.

If both miss, the original ClassNotFoundException is rethrown — but the error log now points consumers at the manifest override they need.

The pure candidate-generation logic is extracted into buildConfigClassCandidates(primary, fallback) so it can be unit-tested without an Android Context.

Consumer wiring (only for the white-label case)

<application ...>
  <meta-data
    android:name="com.frontegg.reactnative.BUILD_CONFIG_PACKAGE"
    android:value="com.example.shared" />
</application>

Tests

This PR introduces the first android/src/test/ source set in the module, with testImplementation 'junit:junit:4.13.2'. New UtilsTest.kt covers four scenarios:

  • Single candidate when no fallback configured
  • Single candidate when fallback is blank/whitespace
  • Ordered two-candidate list (applicationId first, fallback second)
  • Deduplication when applicationId equals fallback

Runs via ./gradlew :react-native_frontegg:test.

Manual test plan

  • Default case (applicationId == namespace): example app builds and initialises FronteggRNModule without error; no behaviour change vs. master.
  • White-label case: in a test app with applicationId = com.foo.app and namespace = com.foo.shared + <meta-data android:name="com.frontegg.reactnative.BUILD_CONFIG_PACKAGE" android:value="com.foo.shared" />, the module initialises successfully (before this PR: ClassNotFoundException at init).
  • Misconfigured case: in a white-label app without the meta-data entry, the error log includes the documented snippet pointing the developer at the missing manifest override.

Alternatives considered

  • Programmatic init: FronteggRN.setBuildConfigClass(BuildConfig::class.java) from MainApplication.onCreate. More explicit, but adds a required call site for every host app and an ordering constraint (must run before the JS engine attaches the module).
  • Gradle property: read the namespace at build time and emit it as a buildConfigField. Cleaner build-side, but doesn't help apps consuming the prebuilt AAR.

The manifest meta-data approach was the least invasive: zero changes for apps where applicationId == namespace, a single declarative line for everyone else, and no API surface on the JS or Kotlin side.

Related

Part of a small batch we found while integrating the SDK — JVM 17 target and currentActivity deprecation are separate PRs.

airowe added 2 commits May 28, 2026 13:17
`Context.fronteggConstants` resolves the host app's generated BuildConfig
via `Class.forName("${context.packageName}.BuildConfig")`. On apps where
the Gradle `applicationId` (== `context.packageName` at runtime) differs
from the AGP `namespace` (the Java package the generated BuildConfig is
emitted into), this throws `ClassNotFoundException` at module init time.

This is a common pattern for white-label / multi-flavor RN apps that
share a Java source tree but ship under different applicationIds.

Resolution is now a small, ordered fallback:

  1. Existing behaviour — try `<applicationId>.BuildConfig` first.
  2. If that misses, look up a `<meta-data>` entry on `<application>`
     named `com.frontegg.reactnative.BUILD_CONFIG_PACKAGE` and try
     `<that value>.BuildConfig`.

If both miss, the existing `ClassNotFoundException` is rethrown — but
the error log now points consumers at the manifest override they need.

For apps where applicationId == namespace (the common case) this is a
no-op: the first candidate matches and the meta-data lookup never runs.

Consumer wiring for the white-label case:

    <application ...>
      <meta-data
        android:name="com.frontegg.reactnative.BUILD_CONFIG_PACKAGE"
        android:value="com.example.shared" />
    </application>
Extracts the pure candidate-list logic from `resolveBuildConfigClass`
into `buildConfigClassCandidates(primary, fallback)` so it can be
exercised without an Android `Context`, then adds a JUnit 4 test suite
covering:

- single candidate when no fallback is configured
- single candidate when fallback is present but blank/whitespace
- ordered two-candidate list (applicationId first, fallback second)
- deduplication when applicationId equals fallback

`resolveBuildConfigClass` keeps its existing signature and continues
to read the manifest meta-data via `Context.packageManager`; only the
candidate generation moves into the testable helper.

Also adds `testImplementation 'junit:junit:4.13.2'` — first test
dependency in this module, so `android/src/test/` source set is now
active. Runs via the standard `./gradlew test` task.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant