@@ -49,8 +49,10 @@ import com.descope.session.DescopeSession
4949import com.descope.session.Token
5050import com.descope.types.AuthenticationResponse
5151import com.descope.types.DescopeException
52+ import kotlinx.coroutines.CoroutineScope
5253import kotlinx.coroutines.Dispatchers
53- import kotlinx.coroutines.GlobalScope
54+ import kotlinx.coroutines.Job
55+ import kotlinx.coroutines.Runnable
5456import kotlinx.coroutines.launch
5557import org.json.JSONObject
5658import java.lang.ref.WeakReference
@@ -59,6 +61,9 @@ import java.util.Timer
5961import java.util.TimerTask
6062import kotlin.concurrent.timer
6163
64+ private const val retryWindow = 10 * 1000L
65+ private const val retryInterval = 1250L
66+
6267@SuppressLint(" SetJavaScriptEnabled" )
6368class DescopeFlowCoordinator (val webView : WebView ) {
6469
@@ -75,6 +80,9 @@ class DescopeFlowCoordinator(val webView: WebView) {
7580 get() = if (flow?.sessionProvider != null ) flow?.sessionProvider?.invoke() else sdk?.sessionManager?.session?.takeIf { ! it.refreshToken.isExpired }
7681 private var currentFlowUrl: Uri ? = null
7782 private var alreadySetUp = false
83+ private var startedAt: Long = 0L
84+ private var attempts: Int = 0
85+ private var loadFailure: Boolean = false
7886
7987 init {
8088 webView.settings.javaScriptEnabled = true
@@ -127,11 +135,7 @@ class DescopeFlowCoordinator(val webView: WebView) {
127135 return
128136 }
129137 currentFlowUrl = url.toUri()
130- val scope = webView.findViewTreeLifecycleOwner()?.lifecycleScope
131- if (scope == null ) {
132- logger.error(" Unable to find lifecycle owner coroutine scope" )
133- return
134- }
138+ val scope = webView.findViewTreeLifecycleOwner()?.lifecycleScope ? : CoroutineScope (Job ())
135139 scope.launch(Dispatchers .Main ) {
136140 var type: String
137141 var canceled = false
@@ -179,7 +183,7 @@ class DescopeFlowCoordinator(val webView: WebView) {
179183 type = " failure"
180184 val failure = when (e) {
181185 DescopeException .oauthNativeCancelled -> {
182- logger.info(" OAuth native canceled" )
186+ logger.info(" OAuth native canceled" )
183187 canceled = true
184188 " OAuthNativeCancelled"
185189 }
@@ -243,6 +247,11 @@ class DescopeFlowCoordinator(val webView: WebView) {
243247 }
244248
245249 override fun onPageFinished (view : WebView ? , url : String? ) {
250+ if (loadFailure) {
251+ logger.info(" Page finished after error" , url)
252+ loadFailure = false
253+ return
254+ }
246255 logger.info(" On page finished" , url, view?.progress)
247256 if (alreadySetUp) {
248257 logger.error(" Bridge is already set up" , url, view?.progress)
@@ -271,6 +280,7 @@ class DescopeFlowCoordinator(val webView: WebView) {
271280 override fun onReceivedError (view : WebView ? , request : WebResourceRequest ? , error : WebResourceError ? ) {
272281 if (request?.isForMainFrame == true ) {
273282 logger.error(" Error loading flow page" , error?.errorCode, error?.description)
283+ if (scheduleRetryAfterError()) return
274284 val code = error?.errorCode ? : 0
275285 val failure = error?.description?.toString() ? : " "
276286 val message = when (code) {
@@ -288,11 +298,28 @@ class DescopeFlowCoordinator(val webView: WebView) {
288298 if (request?.isForMainFrame == true ) {
289299 logger.error(" Flow page failed to load" , errorResponse?.statusCode)
290300 val statusCode = errorResponse?.statusCode ? : 0
301+ if (statusCode >= 500 && scheduleRetryAfterError()) return // potentially retry only on server errors
291302 val message = failureFromResponseCode(statusCode)
292303 val exception = DescopeException .networkError.with (message = message)
293304 handleError(exception)
294305 }
295306 }
307+
308+ private fun scheduleRetryAfterError (): Boolean {
309+ val retryIn = attempts * retryInterval
310+ if (
311+ alreadySetUp // initial loading was successful
312+ || System .currentTimeMillis() - startedAt + retryIn > retryWindow // or retry window exceeded
313+ ) {
314+ return false
315+ }
316+
317+ loadFailure = true
318+ logger.info(" Will retry to load in $retryIn ms" )
319+ val ref = WeakReference (this @DescopeFlowCoordinator)
320+ handler.postDelayed(createRetryRunnable(ref), retryIn)
321+ return true
322+ }
296323 }
297324 }
298325
@@ -324,11 +351,18 @@ document.head.appendChild(element)
324351
325352 // Internal API
326353
327- internal fun run (flow : DescopeFlow ) {
354+ internal fun startFlow (flow : DescopeFlow ) {
328355 this .flow = flow
329356 handleStarted()
330357 webView.loadUrl(flow.url)
331358 }
359+
360+ internal fun reloadFlow () {
361+ val flowUrl = flow?.url ? : return
362+ attempts++
363+ logger.info(" Retrying to load flow (attempt $attempts )" )
364+ webView.loadUrl(flowUrl)
365+ }
332366
333367 internal fun resumeFromDeepLink (deepLink : Uri ) {
334368 if (flow == null ) {
@@ -364,6 +398,8 @@ document.head.appendChild(element)
364398 if (state == Started ) return
365399 state = Started
366400 alreadySetUp = false
401+ startedAt = System .currentTimeMillis()
402+ attempts++
367403 executeHooks(Event .Started )
368404 }
369405
@@ -378,6 +414,7 @@ document.head.appendChild(element)
378414 logger.info(" Flow is ready ($tag )" )
379415 startTimer()
380416 state = Ready
417+ attempts = 0
381418 executeHooks(Event .Ready )
382419 listener?.onReady()
383420 }
@@ -391,20 +428,22 @@ document.head.appendChild(element)
391428 if (state == Failed ) return
392429
393430 handler.post {
394- logger.error(" Flow failed with ${e.code} ) error" , e)
431+ logger.error(" Flow failed with [ ${e.code} ] error" , e)
395432 stopTimer()
396433 state = Failed
434+ attempts = 0
397435 listener?.onError(e)
398436 }
399437 }
400438
401439 private fun handleSuccess (authResponse : AuthenticationResponse ) {
402- if (ensureState(Ready )) return
440+ if (ensureState(Started , Ready )) return
403441 handler.post {
404442 val res = if (logger.isUnsafeEnabled) authResponse else null
405443 logger.info(" Flow finished successfully" , res)
406444 stopTimer()
407445 state = Finished
446+ attempts = 0
408447 listener?.onSuccess(authResponse)
409448 }
410449 }
@@ -661,3 +700,16 @@ private fun createTimerAction(ref: WeakReference<DescopeFlowCoordinator>): (Time
661700 }
662701 }
663702}
703+
704+ private fun createRetryRunnable (ref : WeakReference <DescopeFlowCoordinator >): (Runnable ) {
705+ return object : Runnable {
706+ override fun run () {
707+ val coordinator = ref.get()
708+ if (coordinator == null ) {
709+ return
710+ } else {
711+ coordinator.reloadFlow()
712+ }
713+ }
714+ }
715+ }
0 commit comments