Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ 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.18'

testImplementation 'junit:junit:4.13.2'
}

if (isNewArchitectureEnabled()) {
Expand Down
118 changes: 91 additions & 27 deletions android/src/main/java/com/frontegg/reactnative/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,114 @@ package com.frontegg.reactnative

import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.util.Log


const val TAG: String = "FronteggUtils"

private const val BUILD_CONFIG_PACKAGE_META = "com.frontegg.reactnative.BUILD_CONFIG_PACKAGE"


interface ActivityProvider {
fun getActivity(): Activity?
}

val Context.fronteggConstants: FronteggConstants
get() {
val packageName = this.packageName
val className = "$packageName.BuildConfig"
val buildConfigClass = resolveBuildConfigClass(this)

// Get the field from BuildConfig class
val baseUrl = safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_DOMAIN", "")
val clientId = safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_CLIENT_ID", "")

val applicationId =
safeGetNullableValueFromBuildConfig(buildConfigClass, "FRONTEGG_APPLICATION_ID", "")

val useAssetsLinks =
safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_USE_ASSETS_LINKS", true)
val useChromeCustomTabs = safeGetValueFromBuildConfig(
buildConfigClass, "FRONTEGG_USE_CHROME_CUSTOM_TABS", true
)

return FronteggConstants(
baseUrl = baseUrl,
clientId = clientId,
applicationId = applicationId,
useAssetsLinks = useAssetsLinks,
useChromeCustomTabs = useChromeCustomTabs,
bundleId = this.packageName,
)
}

/**
* Locate the host app's generated `BuildConfig` class.
*
* The class lives at `<javaPackage>.BuildConfig`, but apps whose `applicationId` differs from
* their AGP `namespace` (a common pattern for white-label / multi-flavor builds) need to
* override the lookup, since [Context.getPackageName] returns the `applicationId` at runtime
* while the generated `BuildConfig` is emitted under the `namespace`.
*
* Resolution order:
* 1. `<applicationContext.packageName>.BuildConfig` (the existing behaviour).
* 2. The value of the `com.frontegg.reactnative.BUILD_CONFIG_PACKAGE` `<meta-data>` entry on
* `<application>` in `AndroidManifest.xml`, suffixed with `.BuildConfig`.
*/
private fun resolveBuildConfigClass(context: Context): Class<*> {
val candidates = buildConfigClassCandidates(
primaryPackageName = context.packageName,
fallbackPackageName = readFallbackBuildConfigPackage(context),
)

var lastError: ClassNotFoundException? = null
for (className in candidates) {
try {
val buildConfigClass = Class.forName(className)

// Get the field from BuildConfig class
val baseUrl = safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_DOMAIN", "")
val clientId = safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_CLIENT_ID", "")

val applicationId =
safeGetNullableValueFromBuildConfig(buildConfigClass, "FRONTEGG_APPLICATION_ID", "")

val useAssetsLinks =
safeGetValueFromBuildConfig(buildConfigClass, "FRONTEGG_USE_ASSETS_LINKS", true)
val useChromeCustomTabs = safeGetValueFromBuildConfig(
buildConfigClass, "FRONTEGG_USE_CHROME_CUSTOM_TABS", true
)

return FronteggConstants(
baseUrl = baseUrl,
clientId = clientId,
applicationId = applicationId,
useAssetsLinks = useAssetsLinks,
useChromeCustomTabs = useChromeCustomTabs,
bundleId = this.packageName,
)
return Class.forName(className)
} catch (e: ClassNotFoundException) {
Log.e(TAG, "Class not found: $className")
throw e
Log.w(TAG, "BuildConfig not found at $className, trying next candidate")
lastError = e
}
}

Log.e(
TAG,
"Could not locate host BuildConfig. Tried: ${candidates.joinToString()}. " +
"If applicationId differs from your android.namespace, add " +
"<meta-data android:name=\"$BUILD_CONFIG_PACKAGE_META\" " +
"android:value=\"your.java.package\" /> to <application> in AndroidManifest.xml."
)
throw lastError ?: ClassNotFoundException("BuildConfig not found for ${context.packageName}")
}

/**
* Pure-logic candidate list for `BuildConfig` class lookup. Extracted from
* [resolveBuildConfigClass] so it can be unit-tested without Android dependencies.
*
* Guarantees:
* - `primaryPackageName` is always tried first.
* - `fallbackPackageName`, if non-null and non-blank, is appended next.
* - If both inputs produce the same candidate string, only the first occurrence is kept.
*/
internal fun buildConfigClassCandidates(
primaryPackageName: String,
fallbackPackageName: String?,
): List<String> {
val candidates = LinkedHashSet<String>()
candidates.add("$primaryPackageName.BuildConfig")
fallbackPackageName?.takeIf { it.isNotBlank() }?.let {
candidates.add("$it.BuildConfig")
}
return candidates.toList()
}

private fun readFallbackBuildConfigPackage(context: Context): String? = runCatching {
val appInfo = context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA,
)
appInfo.metaData?.getString(BUILD_CONFIG_PACKAGE_META)
}.getOrNull()

fun <T> safeGetNullableValueFromBuildConfig(
buildConfigClass: Class<*>,
name: String,
Expand Down
53 changes: 53 additions & 0 deletions android/src/test/java/com/frontegg/reactnative/UtilsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.frontegg.reactnative

import org.junit.Assert.assertEquals
import org.junit.Test

class UtilsTest {

@Test
fun `single candidate when fallback is null`() {
val candidates = buildConfigClassCandidates(
primaryPackageName = "com.example.app",
fallbackPackageName = null,
)

assertEquals(listOf("com.example.app.BuildConfig"), candidates)
}

@Test
fun `single candidate when fallback is blank`() {
val candidates = buildConfigClassCandidates(
primaryPackageName = "com.example.app",
fallbackPackageName = " ",
)

assertEquals(listOf("com.example.app.BuildConfig"), candidates)
}

@Test
fun `applicationId is always tried before fallback`() {
val candidates = buildConfigClassCandidates(
primaryPackageName = "com.example.app",
fallbackPackageName = "com.example.shared",
)

assertEquals(
listOf(
"com.example.app.BuildConfig",
"com.example.shared.BuildConfig",
),
candidates,
)
}

@Test
fun `duplicate candidate is deduplicated when applicationId equals fallback`() {
val candidates = buildConfigClassCandidates(
primaryPackageName = "com.example.shared",
fallbackPackageName = "com.example.shared",
)

assertEquals(listOf("com.example.shared.BuildConfig"), candidates)
}
}