Skip to content

Commit

Permalink
Merge pull request #64 from frontegg/FR-17426-fix-refresh-token-in-ba…
Browse files Browse the repository at this point in the history
…ckground

FR-17426 - Comprehensive Enhancements and Bug Fixes
  • Loading branch information
frontegg-david authored Aug 29, 2024
2 parents 26a45d6 + 9d87be1 commit 9843595
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 57 deletions.
6 changes: 6 additions & 0 deletions android/src/main/java/com/frontegg/android/FronteggApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.frontegg.android.exceptions.FronteggException
import com.frontegg.android.exceptions.FronteggException.Companion.FRONTEGG_APP_MUST_BE_INITIALIZED
import com.frontegg.android.regions.RegionConfig
import com.frontegg.android.services.*
import java.time.Instant

class FronteggApp private constructor(
val context: Context,
Expand All @@ -34,7 +35,9 @@ class FronteggApp private constructor(
val auth: FronteggAuth =
FronteggAuth(baseUrl, clientId, applicationId, credentialManager, regions, selectedRegion)
val packageName: String = context.packageName
var appInForeground = true

var lastJobStart:Long = Instant.now().toEpochMilli();
companion object {

@SuppressLint("StaticFieldLeak")
Expand Down Expand Up @@ -129,9 +132,12 @@ class FronteggApp private constructor(
when (event) {
Lifecycle.Event.ON_STOP -> {
Log.d(TAG, "ON_STOP")
getInstance().appInForeground = false
getInstance().auth.refreshTokenWhenNeeded()
}
Lifecycle.Event.ON_START -> {
Log.d(TAG, "ON_START")
getInstance().appInForeground = true
getInstance().auth.refreshTokenWhenNeeded()
}
else -> {}
Expand Down
107 changes: 74 additions & 33 deletions android/src/main/java/com/frontegg/android/FronteggAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.time.Instant
import java.util.Timer
import java.util.TimerTask
import kotlin.concurrent.schedule


@SuppressLint("CheckResult")
Expand Down Expand Up @@ -59,9 +62,11 @@ class FronteggAuth(
val isLoading: ObservableValue<Boolean> = ObservableValue(true)
val initializing: ObservableValue<Boolean> = ObservableValue(true)
val showLoader: ObservableValue<Boolean> = ObservableValue(true)
val refreshingToken: ObservableValue<Boolean> = ObservableValue(false)
var pendingAppLink: String? = null
val isMultiRegion: Boolean = regions.isNotEmpty()
var refreshTokenJob: JobInfo? = null;
var timerTask: TimerTask? = null;
private var _api: Api? = null

init {
Expand Down Expand Up @@ -113,42 +118,47 @@ class FronteggAuth(
if (!refreshTokenIfNeeded()) {
accessToken.value = null
refreshToken.value = null
isLoading.value = false
initializing.value = false
isLoading.value = false
}

} else {
isLoading.value = false
initializing.value = false
isLoading.value = false
}
}
}

fun sendRefreshToken(): Boolean {
val refreshToken = this.refreshToken.value ?: return false
val data = api.refreshToken(refreshToken)
return if (data != null) {
setCredentials(data.access_token, data.refresh_token)
true
} else {
Log.e(TAG, "Failed to refresh token, data = null")
false
this.refreshingToken.value = true
try {

val data = api.refreshToken(refreshToken)
return if (data != null) {
setCredentials(data.access_token, data.refresh_token)
true
} else {
Log.e(TAG, "Failed to refresh token, data = null")
false
}
} finally {
this.refreshingToken.value = false
}
}

fun refreshTokenWhenNeeded() {
val accessToken = this.accessToken.value

if(this.refreshToken.value == null){
if (this.refreshToken.value == null) {
return
}


if (refreshTokenJob != null) {
cancelLastTimer()
}
if(accessToken == null){
// when we have valid refreshToken without accessToken => failed to refresh in background
cancelLastTimer()

if (accessToken == null) {
// when we have valid refreshToken without accessToken => failed to refresh in background
GlobalScope.launch(Dispatchers.IO) {
sendRefreshToken()
}
Expand Down Expand Up @@ -220,24 +230,46 @@ class FronteggAuth(

private fun cancelLastTimer() {
Log.d(TAG, "Cancel Last Timer")
val context = FronteggApp.getInstance().context
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
jobScheduler.cancel(JOB_ID)
this.refreshTokenJob = null
if(timerTask!= null){
timerTask?.cancel()
timerTask = null
}
if(refreshTokenJob!= null) {
val context = FronteggApp.getInstance().context
val jobScheduler =
context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
jobScheduler.cancel(JOB_ID)
this.refreshTokenJob = null
}
}

private fun scheduleTimer(offset: Long) {
Log.d(TAG, "Schedule Timer")
val context = FronteggApp.getInstance().context
val jobScheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
// Schedule the job
val jobInfo = JobInfo.Builder(
JOB_ID, ComponentName(context, RefreshTokenService::class.java)
).setMinimumLatency(offset) // Schedule the job to run after the offset
.setOverrideDeadline(offset + 10000) // Add a buffer to the deadline
.build()
this.refreshTokenJob = jobInfo
jobScheduler.schedule(jobInfo)
fun scheduleTimer(offset: Long) {
FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli()
if(FronteggApp.getInstance().appInForeground){
Log.d(TAG, "[foreground] Start Timer task (${offset} ms)")

this.timerTask = Timer().schedule(offset) {
Log.d(TAG, "[foreground] Job started, (${Instant.now().toEpochMilli()-FronteggApp.getInstance().lastJobStart} ms)")
refreshTokenIfNeeded()
}

}else {
Log.d(TAG, "[background] Start Job Scheduler task (${offset} ms)")
val context = FronteggApp.getInstance().context
val jobScheduler =
context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
// Schedule the job
val jobInfo = JobInfo.Builder(
JOB_ID, ComponentName(context, RefreshTokenService::class.java)
)
.setMinimumLatency(offset / 2) // Schedule the job to run after the offset
.setOverrideDeadline(offset) // Add a buffer to the deadline
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) // Require network
.setBackoffCriteria(10000, JobInfo.BACKOFF_POLICY_LINEAR)
.build()
this.refreshTokenJob = jobInfo
jobScheduler.schedule(jobInfo)
}
}

fun handleHostedLoginCallback(
Expand Down Expand Up @@ -357,8 +389,17 @@ class FronteggAuth(
}
}

private fun calculateTimerOffset(exp: Long): Long {
private fun calculateTimerOffset(expirationTime: Long): Long {
val now: Long = Instant.now().toEpochMilli()
return (((exp * 1000) - now) * 0.80).toLong()
val remainingTime = (expirationTime * 1000) - now

val minRefreshWindow = 20000 // minimum 20 seconds before exp
val adaptiveRefreshTime = remainingTime * 0.8 // 80% of remaining time

return if (remainingTime > minRefreshWindow) {
adaptiveRefreshTime.toLong()
} else {
(remainingTime - minRefreshWindow).coerceAtLeast(0)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
private val TAG = FronteggWebClient::class.java.simpleName
}

var webViewStatusCode: Int = 200
var lastErrorResponse: WebResourceResponse? = null

override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
Log.d(TAG, "onPageStarted $url")
FronteggAuth.instance.isLoading.value = true

}

override fun onPageFinished(view: WebView?, url: String?) {
Expand All @@ -65,16 +67,26 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
return
}

val fronteggApp = FronteggApp.getInstance()
val nativeModuleFunctions = JSONObject()
nativeModuleFunctions.put("loginWithSocialLogin", fronteggApp.handleLoginWithSocialLogin)
nativeModuleFunctions.put("loginWithSSO", fronteggApp.handleLoginWithSSO)
nativeModuleFunctions.put(
"shouldPromptSocialLoginConsent",
fronteggApp.shouldPromptSocialLoginConsent
)
val jsObject = nativeModuleFunctions.toString()
view?.evaluateJavascript("window.FronteggNativeBridgeFunctions = ${jsObject};", null)

if (webViewStatusCode >= 400) {
checkIfFronteggError(view, url, lastErrorResponse)
webViewStatusCode = 200
lastErrorResponse = null
} else {
val fronteggApp = FronteggApp.getInstance()
val nativeModuleFunctions = JSONObject()
nativeModuleFunctions.put(
"loginWithSocialLogin",
fronteggApp.handleLoginWithSocialLogin
)
nativeModuleFunctions.put("loginWithSSO", fronteggApp.handleLoginWithSSO)
nativeModuleFunctions.put(
"shouldPromptSocialLoginConsent",
fronteggApp.shouldPromptSocialLoginConsent
)
val jsObject = nativeModuleFunctions.toString()
view?.evaluateJavascript("window.FronteggNativeBridgeFunctions = ${jsObject};", null)
}

}

Expand Down Expand Up @@ -114,25 +126,49 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
super.onReceivedError(view, request, error)
}

private fun checkIfFronteggError(view: WebView?, url: String?, status: Int? = null) {
private fun checkIfFronteggError(
view: WebView?,
url: String?,
errorResponse: WebResourceResponse?
) {
if (view == null || url == null) {
return
}
val status = errorResponse?.statusCode

if (url.startsWith("${FronteggAuth.instance.baseUrl}/oauth/authorize")) {
val reloadScript =
"setTimeout(()=>window.location.href=\"${url.replace("\"", "\\\"")}\", 4000)"
val jsCode = "(function(){\n" +
" var script = document.createElement('script');\n" +
" script.innerHTML=`$reloadScript`;" +
" document.body.appendChild(script)\n" +
" })()"
view.evaluateJavascript(jsCode, null)
FronteggAuth.instance.isLoading.value = false
return
}


view.evaluateJavascript("document.body.innerText") { result ->
try {
var text = result
if (text == null) {
return@evaluateJavascript
}

var error = ""

var json = JsonParser.parseString(text)
while (!json.isJsonObject) {
text = json.asString
json = JsonParser.parseString(text)
}
val error = json.asJsonObject.get("errors").asJsonArray.map {
error = json.asJsonObject.get("errors").asJsonArray.map {
it.asString
}.joinToString("\n")


Log.e(TAG, "Frontegg ERROR: $error")

val htmlError = Html.escapeHtml(error)
Expand All @@ -153,12 +189,22 @@ class FronteggWebClient(val context: Context) : WebViewClient() {
request: WebResourceRequest?,
errorResponse: WebResourceResponse?
) {
if (view!!.url == request?.url.toString()) {
Log.d(TAG, "onReceivedHttpError: Direct load url, ${request?.url?.path}")

if (view == null || request == null) {
return
}
val requestUrl = request.url
val authorizeUrlPrefix = "${FronteggAuth.instance.baseUrl}/oauth/authorize"
if (view.url == requestUrl.toString()
|| requestUrl.toString().startsWith(authorizeUrlPrefix)
) {
Log.d(TAG, "onReceivedHttpError: Direct load url, ${requestUrl.path}")
webViewStatusCode = errorResponse?.statusCode ?: 200
lastErrorResponse = errorResponse
} else {
Log.d(TAG, "onReceivedHttpError: HTTP api call, ${request?.url?.path}")
}
checkIfFronteggError(view, request?.url.toString(), errorResponse?.statusCode)

super.onReceivedHttpError(view, request, errorResponse)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ package com.frontegg.android.services
import android.app.job.JobParameters
import android.app.job.JobService
import android.util.Log
import com.frontegg.android.FronteggApp
import com.frontegg.android.FronteggAuth
import java.net.SocketTimeoutException
import java.time.Instant

class RefreshTokenService : JobService() {

Expand All @@ -14,20 +16,13 @@ class RefreshTokenService : JobService() {
}

override fun onStartJob(params: JobParameters?): Boolean {
Log.d(TAG, "Job started")

// Perform your background task here
Log.d(TAG, "Job started, (${Instant.now().toEpochMilli() - FronteggApp.getInstance().lastJobStart} ms)")
performBackgroundTask(params)

// Return true if the job is still running, false if it is completed
// This example returns false, indicating the job is finished
return true
}

override fun onStopJob(params: JobParameters?): Boolean {
Log.d(TAG, "Job stopped before completion")

// Cleanup if necessary. Return true to reschedule the job if needed
return false
}

Expand All @@ -45,7 +40,11 @@ class RefreshTokenService : JobService() {
FronteggAuth.instance.accessToken.value = null
FronteggAuth.instance.isLoading.value = true
isError = true
if(e is SocketTimeoutException) {
FronteggAuth.instance.scheduleTimer(20000)
}
} finally {
FronteggApp.getInstance().lastJobStart = Instant.now().toEpochMilli()
// Notify the job manager that the job has been completed
jobFinished(params, isError)
}
Expand Down

0 comments on commit 9843595

Please sign in to comment.