Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/workflows/react-native-sdk-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22

- name: Install JS dependencies
run: |
Expand Down Expand Up @@ -170,7 +170,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22

- name: Install JS dependencies
run: |
Expand Down
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
18.10.0
22
37 changes: 36 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}

def supportsNamespace() {
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
def major = parsed[0].toInteger()
def minor = parsed[1].toInteger()

return (major == 7 && minor >= 3) || major >= 8
}

apply plugin: "com.android.library"
apply plugin: "kotlin-android"

Expand All @@ -37,13 +45,27 @@ def getExtOrIntegerDefault(name) {
}

android {
if (supportsNamespace()) {
namespace "com.frontegg.reactnative"

sourceSets {
main {
manifest.srcFile "src/main/AndroidManifestNew.xml"
}
}
}

compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")

defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
}
buildFeatures {
buildConfig true
}

buildTypes {
release {
minifyEnabled false
Expand All @@ -68,6 +90,19 @@ android {
useJUnit()
}
}

sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"src/newarch",
"${project.buildDir}/generated/source/codegen/java"
]
} else {
java.srcDirs += ["src/oldarch"]
}
}
}
}

repositories {
Expand All @@ -93,7 +128,7 @@ dependencies {
if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
libraryName = "ReactNative"
libraryName = "RNFronteggRNSpec"
codegenJavaPackageName = "com.frontegg.reactnative"
}
}
17 changes: 17 additions & 0 deletions android/src/main/AndroidManifestNew.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

<application>

</application>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>

</manifest>
102 changes: 102 additions & 0 deletions android/src/main/java/com/frontegg/reactnative/AccessTokenRefresher.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.frontegg.reactnative

import android.content.Context
import com.frontegg.android.FronteggAuth
import com.frontegg.android.utils.FronteggConstantsProvider
import com.frontegg.android.utils.JWTHelper
import com.frontegg.android.utils.NetworkGate
import java.io.IOException
import java.time.Instant
import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull

private const val MINIMUM_ACCESS_TOKEN_VALIDITY_SECONDS = 15
private const val AUTH_STATE_SETTLE_TIMEOUT_MS = 20_000L
private const val CONCURRENT_REFRESH_TIMEOUT_MS = 10_000L
private const val AUTH_STATE_POLL_INTERVAL_MS = 200L

internal suspend fun fetchValidAccessToken(auth: FronteggAuth, context: Context): String? {
if (auth.isMultiRegion && auth.selectedRegion == null) {
return null
}
Comment thread
dianaKhortiuk-frontegg marked this conversation as resolved.
Resolved
val settled = waitWhile(AUTH_STATE_SETTLE_TIMEOUT_MS) {
auth.initializing.value || auth.refreshingToken.value
}
if (!settled) {
throw IllegalStateException("Timed out waiting for authentication state to settle")
}
if (auth.refreshToken.value == null) {
return null
}

val currentAccessToken = auth.accessToken.value
if (hasMoreThanMinimumValidity(currentAccessToken)) {
return currentAccessToken
}

val offlineModeEnabled = isOfflineModeEnabled(context)
if (!NetworkGate.isNetworkLikelyGood(context)) {
if (offlineModeEnabled) {
return currentAccessToken
}
throw IOException("No network connection to refresh the access token")
}

val refreshedAccessToken = refreshUntilValid(auth)
if (refreshedAccessToken != null) {
return refreshedAccessToken
}
if (auth.refreshToken.value == null) {
return null
}
if (offlineModeEnabled && !NetworkGate.isNetworkLikelyGood(context)) {
return auth.accessToken.value
}
throw IOException("Access token could not be refreshed")
}

private suspend fun refreshUntilValid(auth: FronteggAuth): String? {
if (!auth.refreshTokenAndWait()) {
return null
}
if (hasMoreThanMinimumValidity(auth.accessToken.value)) {
return auth.accessToken.value
}
waitWhile(CONCURRENT_REFRESH_TIMEOUT_MS) { auth.refreshingToken.value }
if (hasMoreThanMinimumValidity(auth.accessToken.value)) {
return auth.accessToken.value
}
if (!auth.refreshTokenAndWait()) {
return null
}
return auth.accessToken.value?.takeIf { hasMoreThanMinimumValidity(it) }
}

private suspend fun waitWhile(timeoutMs: Long, condition: () -> Boolean): Boolean {
val finished = withTimeoutOrNull(timeoutMs) {
while (condition()) {
delay(AUTH_STATE_POLL_INTERVAL_MS)
}
true
}
return finished == true
}

// Same source the SDK is initialized from, both by its auto-init and by RegionsInitializer.
private fun isOfflineModeEnabled(context: Context): Boolean {
return runCatching {
FronteggConstantsProvider.fronteggConstants(context).enableOfflineMode
}.getOrDefault(false)
}

private fun hasMoreThanMinimumValidity(accessToken: String?): Boolean {
if (accessToken == null) {
return false
}
return try {
val secondsLeft = JWTHelper.decode(accessToken).exp - Instant.now().epochSecond
secondsLeft > MINIMUM_ACCESS_TOKEN_VALIDITY_SECONDS
} catch (e: Exception) {
false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ data class FronteggConstants(
val applicationId: String?,
val useAssetsLinks: Boolean,
val useChromeCustomTabs: Boolean,
val bundleId: String
val bundleId: String,
val regionsJson: String? = null,
) {
fun toMap(): MutableMap<String, Any?> {
return mutableMapOf(
Expand Down
Loading
Loading