diff --git a/android/build.gradle b/android/build.gradle index d5f60b4..66d5921 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -62,6 +62,12 @@ android { kotlinOptions { jvmTarget = "17" } + + testOptions { + unitTests.all { + useJUnit() + } + } } repositories { @@ -81,6 +87,7 @@ dependencies { implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1' implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.frontegg.sdk:android:1.3.34' + testImplementation "junit:junit:4.13.2" } if (isNewArchitectureEnabled()) { diff --git a/android/src/main/java/com/frontegg/reactnative/BuildConfigResolver.kt b/android/src/main/java/com/frontegg/reactnative/BuildConfigResolver.kt new file mode 100644 index 0000000..3c51ec9 --- /dev/null +++ b/android/src/main/java/com/frontegg/reactnative/BuildConfigResolver.kt @@ -0,0 +1,75 @@ +package com.frontegg.reactnative + +import android.content.Context +import android.content.pm.PackageManager +import android.util.Log + +/** Manifest meta-data name for the package that contains the host app [BuildConfig]. */ +const val BUILD_CONFIG_PACKAGE_META_DATA_NAME = "com.frontegg.reactnative.BUILD_CONFIG_PACKAGE" + +/** + * Ordered FQCN candidates for the host app [BuildConfig] class. + * + * @param applicationId Runtime application id ([Context.getPackageName]). + * @param manifestBuildConfigPackage Optional value from [BUILD_CONFIG_PACKAGE_META_DATA_NAME]. + */ +fun buildConfigClassCandidates( + applicationId: String, + manifestBuildConfigPackage: String?, +): List { + val candidates = mutableListOf("$applicationId.BuildConfig") + + val packageName = manifestBuildConfigPackage?.trim()?.takeIf { it.isNotEmpty() } ?: return candidates + + val className = + if (packageName.endsWith(".BuildConfig")) packageName else "$packageName.BuildConfig" + + if (className !in candidates) { + candidates.add(className) + } + + return candidates +} + +fun resolveBuildConfigClass( + applicationId: String, + manifestBuildConfigPackage: String?, +): Class<*> { + val candidates = buildConfigClassCandidates(applicationId, manifestBuildConfigPackage) + var lastError: ClassNotFoundException? = null + + for (className in candidates) { + try { + return Class.forName(className) + } catch (e: ClassNotFoundException) { + Log.w(TAG, "BuildConfig not found at $className, trying next candidate") + lastError = e + } + } + + val hint = + if (manifestBuildConfigPackage.isNullOrBlank()) { + "If applicationId differs from your AGP namespace, add under ." + } else { + "Verify " + + "points to the package that contains BuildConfig." + } + + Log.e(TAG, "Could not load BuildConfig. Tried: ${candidates.joinToString()}. $hint") + + throw lastError ?: ClassNotFoundException( + "No BuildConfig class found for applicationId=$applicationId" + ) +} + +fun Context.readBuildConfigPackageMetaData(): String? { + return try { + val appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) + appInfo.metaData?.getString(BUILD_CONFIG_PACKAGE_META_DATA_NAME) + } catch (e: Exception) { + Log.w(TAG, "Failed to read $BUILD_CONFIG_PACKAGE_META_DATA_NAME from manifest", e) + null + } +} diff --git a/android/src/main/java/com/frontegg/reactnative/Utils.kt b/android/src/main/java/com/frontegg/reactnative/Utils.kt index 6a7357b..9f1f029 100644 --- a/android/src/main/java/com/frontegg/reactnative/Utils.kt +++ b/android/src/main/java/com/frontegg/reactnative/Utils.kt @@ -15,9 +15,11 @@ interface ActivityProvider { val Context.fronteggConstants: FronteggConstants get() { val packageName = this.packageName - val className = "$packageName.BuildConfig" try { - val buildConfigClass = Class.forName(className) + val buildConfigClass = resolveBuildConfigClass( + packageName, + readBuildConfigPackageMetaData(), + ) // Get the field from BuildConfig class val baseUrl = safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_DOMAIN", "") @@ -41,7 +43,6 @@ val Context.fronteggConstants: FronteggConstants bundleId = this.packageName, ) } catch (e: ClassNotFoundException) { - Log.e(TAG, "Class not found: $className") throw e } } diff --git a/android/src/test/java/com/frontegg/reactnative/BuildConfigResolverTest.kt b/android/src/test/java/com/frontegg/reactnative/BuildConfigResolverTest.kt new file mode 100644 index 0000000..3b62c33 --- /dev/null +++ b/android/src/test/java/com/frontegg/reactnative/BuildConfigResolverTest.kt @@ -0,0 +1,50 @@ +package com.frontegg.reactnative + +import org.junit.Assert.assertEquals +import org.junit.Test + +class BuildConfigResolverTest { + + @Test + fun buildConfigClassCandidates_usesApplicationIdWhenNoMetaData() { + assertEquals( + listOf("com.example.app.BuildConfig"), + buildConfigClassCandidates("com.example.app", null), + ) + } + + @Test + fun buildConfigClassCandidates_appendsMetaDataPackage() { + assertEquals( + listOf( + "com.healthie.app.flavor.BuildConfig", + "com.main.BuildConfig", + ), + buildConfigClassCandidates("com.healthie.app.flavor", "com.main"), + ) + } + + @Test + fun buildConfigClassCandidates_acceptsFullyQualifiedBuildConfigInMetaData() { + assertEquals( + listOf("com.example.app.BuildConfig", "com.main.BuildConfig"), + buildConfigClassCandidates("com.example.app", "com.main.BuildConfig"), + ) + } + + @Test + fun buildConfigClassCandidates_doesNotDuplicateWhenMetaDataMatchesApplicationId() { + assertEquals( + listOf("com.example.app.BuildConfig"), + buildConfigClassCandidates("com.example.app", "com.example.app"), + ) + } + + @Test + fun buildConfigClassCandidates_ignoresBlankMetaData() { + assertEquals( + listOf("com.example.app.BuildConfig"), + buildConfigClassCandidates("com.example.app", " "), + ) + } +} diff --git a/docs/setup.md b/docs/setup.md index 49d7f68..978bd7e 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -208,6 +208,18 @@ android { } ``` +### White-label apps (`applicationId` ≠ `namespace`) + +If your product flavors use different `applicationId` values but share one Java/Kotlin `namespace`, AGP generates `BuildConfig` under the **namespace** package, not under each flavor's `applicationId`. The SDK looks up `BuildConfig` by `applicationId` first; when that fails, add this under `` in `android/app/src/main/AndroidManifest.xml` (use your shared namespace, without `.BuildConfig`): + +```xml + +``` + +Apps where `applicationId` matches `namespace` do not need this entry. + ### Enable Android AssetLinks To enable Android features like Magic Link authentication, password reset, account activation, and login with identity providers, follow the steps below.