diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt index a3aff4e7..2d124aa6 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebFragment.kt @@ -11,6 +11,7 @@ import dev.hotwire.turbo.fragments.TurboWebFragment import dev.hotwire.turbo.nav.TurboNavGraphDestination import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisitAction.REPLACE +import dev.hotwire.turbo.visit.TurboVisitError import dev.hotwire.turbo.visit.TurboVisitOptions @TurboNavGraphDestination(uri = "turbo://fragment/web") @@ -58,10 +59,10 @@ open class WebFragment : TurboWebFragment(), NavDestination { menuProgress?.isVisible = false } - override fun onVisitErrorReceived(location: String, errorCode: Int) { - when (errorCode) { + override fun onVisitErrorReceived(location: String, error: TurboVisitError) { + when (error.code) { 401 -> navigate(SIGN_IN_URL, TurboVisitOptions(action = REPLACE)) - else -> super.onVisitErrorReceived(location, errorCode) + else -> super.onVisitErrorReceived(location, error) } } diff --git a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt index 9f6a069c..b40d577f 100644 --- a/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt +++ b/demo/src/main/kotlin/dev/hotwire/turbo/demo/features/web/WebHomeFragment.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.ViewGroup import dev.hotwire.turbo.demo.R import dev.hotwire.turbo.nav.TurboNavGraphDestination +import dev.hotwire.turbo.visit.TurboVisitError @TurboNavGraphDestination(uri = "turbo://fragment/web/home") class WebHomeFragment : WebFragment() { @@ -15,7 +16,7 @@ class WebHomeFragment : WebFragment() { } @SuppressLint("InflateParams") - override fun createErrorView(statusCode: Int): View { + override fun createErrorView(error: TurboVisitError): View { return layoutInflater.inflate(R.layout.error_web_home, null) } diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt index 40279e8e..9591f25f 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/delegates/TurboWebFragmentDelegate.kt @@ -21,6 +21,7 @@ import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit import dev.hotwire.turbo.visit.TurboVisitAction +import dev.hotwire.turbo.visit.TurboVisitError import dev.hotwire.turbo.visit.TurboVisitOptions import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -158,8 +159,8 @@ internal class TurboWebFragmentDelegate( /** * Displays the error view that's implemented via [TurboWebFragmentCallback.createErrorView]. */ - fun showErrorView(code: Int) { - turboView?.addErrorView(callback.createErrorView(code)) + fun showErrorView(error: TurboVisitError) { + turboView?.addErrorView(callback.createErrorView(error)) } // ----------------------------------------------------------------------- @@ -205,19 +206,19 @@ internal class TurboWebFragmentDelegate( navDestination.fragmentViewModel.setTitle(title()) } - override fun onReceivedError(errorCode: Int) { - callback.onVisitErrorReceived(location, errorCode) + override fun onReceivedError(error: TurboVisitError) { + callback.onVisitErrorReceived(location, error) } override fun onRenderProcessGone() { navigator.navigate(location, TurboVisitOptions(action = TurboVisitAction.REPLACE)) } - override fun requestFailedWithStatusCode(visitHasCachedSnapshot: Boolean, statusCode: Int) { + override fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError) { if (visitHasCachedSnapshot) { - callback.onVisitErrorReceivedWithCachedSnapshotAvailable(location, statusCode) + callback.onVisitErrorReceivedWithCachedSnapshotAvailable(location, error) } else { - callback.onVisitErrorReceived(location, statusCode) + callback.onVisitErrorReceived(location, error) } } diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt index 741812a3..d2f35af1 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebBottomSheetDialogFragment.kt @@ -13,6 +13,7 @@ import dev.hotwire.turbo.delegates.TurboWebFragmentDelegate import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient +import dev.hotwire.turbo.visit.TurboVisitError /** * The base class from which all bottom sheet web fragments in a @@ -82,7 +83,7 @@ abstract class TurboWebBottomSheetDialogFragment : TurboBottomSheetDialogFragmen } @SuppressLint("InflateParams") - override fun createErrorView(statusCode: Int): View { + override fun createErrorView(error: TurboVisitError): View { return layoutInflater.inflate(R.layout.turbo_error, null) } @@ -90,7 +91,7 @@ abstract class TurboWebBottomSheetDialogFragment : TurboBottomSheetDialogFragmen return TurboWebChromeClient(session) } - override fun onVisitErrorReceived(location: String, errorCode: Int) { - webDelegate.showErrorView(errorCode) + override fun onVisitErrorReceived(location: String, error: TurboVisitError) { + webDelegate.showErrorView(error) } } diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt index 79549a35..7b36d7cd 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragment.kt @@ -13,6 +13,7 @@ import dev.hotwire.turbo.session.TurboSessionModalResult import dev.hotwire.turbo.util.TURBO_REQUEST_CODE_FILES import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient +import dev.hotwire.turbo.visit.TurboVisitError /** * The base class from which all web "standard" fragments (non-dialogs) in a @@ -94,7 +95,7 @@ abstract class TurboWebFragment : TurboFragment(), TurboWebFragmentCallback { } @SuppressLint("InflateParams") - override fun createErrorView(statusCode: Int): View { + override fun createErrorView(error: TurboVisitError): View { return layoutInflater.inflate(R.layout.turbo_error, null) } @@ -102,7 +103,7 @@ abstract class TurboWebFragment : TurboFragment(), TurboWebFragmentCallback { return TurboWebChromeClient(session) } - override fun onVisitErrorReceived(location: String, errorCode: Int) { - webDelegate.showErrorView(errorCode) + override fun onVisitErrorReceived(location: String, error: TurboVisitError) { + webDelegate.showErrorView(error) } } diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt index ecb93ccd..64458dd8 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/fragments/TurboWebFragmentCallback.kt @@ -5,6 +5,7 @@ import android.webkit.HttpAuthHandler import dev.hotwire.turbo.views.TurboView import dev.hotwire.turbo.views.TurboWebChromeClient import dev.hotwire.turbo.views.TurboWebView +import dev.hotwire.turbo.visit.TurboVisitError /** * Callback interface to be implemented by a [TurboWebFragment], @@ -19,7 +20,7 @@ interface TurboWebFragmentCallback { /** * Inflate and return a new view to serve as an error view. */ - fun createErrorView(statusCode: Int): View + fun createErrorView(error: TurboVisitError): View /** * Inflate and return a new view to serve as a progress view. @@ -71,7 +72,7 @@ interface TurboWebFragmentCallback { /** * Called when a Turbo visit resulted in an error. */ - fun onVisitErrorReceived(location: String, errorCode: Int) {} + fun onVisitErrorReceived(location: String, error: TurboVisitError) {} /** * Called when a Turbo form submission has started. @@ -87,7 +88,7 @@ interface TurboWebFragmentCallback { * Called when the Turbo visit resulted in an error, but a cached * snapshot is being displayed, which may be stale. */ - fun onVisitErrorReceivedWithCachedSnapshotAvailable(location: String, errorCode: Int) {} + fun onVisitErrorReceivedWithCachedSnapshotAvailable(location: String, error: TurboVisitError) {} /** * Called when the WebView has received an HTTP authentication request. diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt index 9d68cc5e..b4df2c6b 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSession.kt @@ -23,6 +23,8 @@ import dev.hotwire.turbo.util.* import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit import dev.hotwire.turbo.visit.TurboVisitAction +import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.visit.TurboVisitErrorType import dev.hotwire.turbo.visit.TurboVisitOptions import kotlinx.coroutines.* import java.util.* @@ -283,6 +285,12 @@ class TurboSession internal constructor( */ @JavascriptInterface fun visitRequestFailedWithStatusCode(visitIdentifier: String, visitHasCachedSnapshot: Boolean, statusCode: Int) { + val visitError = TurboVisitError( + type = TurboVisitErrorType.HTTP_ERROR, + code = statusCode, + description = "Request failed" + ) + logEvent( "visitRequestFailedWithStatusCode", "visitIdentifier" to visitIdentifier, @@ -292,7 +300,7 @@ class TurboSession internal constructor( currentVisit?.let { visit -> if (visitIdentifier == visit.identifier) { - callback { it.requestFailedWithStatusCode(visitHasCachedSnapshot, statusCode) } + callback { it.requestFailedWithError(visitHasCachedSnapshot, visitError) } } } } @@ -477,9 +485,15 @@ class TurboSession internal constructor( */ @JavascriptInterface fun turboFailedToLoad() { - logEvent("turboFailedToLoad") + val visitError = TurboVisitError( + type = TurboVisitErrorType.LOAD_ERROR, + code = -1, + description = "Turbo failed to load" + ) + + logEvent("turboFailedToLoad", "error" to visitError) reset() - callback { it.onReceivedError(-1) } + callback { it.onReceivedError(visitError) } } /** @@ -748,9 +762,19 @@ class TurboSession internal constructor( super.onReceivedError(view, request, error) if (request.isForMainFrame && isFeatureSupported(WEB_RESOURCE_ERROR_GET_CODE)) { - logEvent("onReceivedError", "errorCode" to error.errorCode) + val visitError = TurboVisitError( + type = TurboVisitErrorType.WEB_RESOURCE_ERROR, + code = error.errorCode, + description = if (isFeatureSupported(WEB_RESOURCE_ERROR_GET_DESCRIPTION)) { + error.description.toString() + } else { + null + } + ) + + logEvent("onReceivedError", "error" to visitError) reset() - callback { it.onReceivedError(error.errorCode) } + callback { it.onReceivedError(visitError) } } } @@ -758,9 +782,15 @@ class TurboSession internal constructor( super.onReceivedHttpError(view, request, errorResponse) if (request.isForMainFrame) { - logEvent("onReceivedHttpError", "statusCode" to errorResponse.statusCode) + val visitError = TurboVisitError( + type = TurboVisitErrorType.HTTP_ERROR, + code = errorResponse.statusCode, + description = errorResponse.reasonPhrase + ) + + logEvent("onReceivedHttpError", "error" to visitError) reset() - callback { it.onReceivedError(errorResponse.statusCode) } + callback { it.onReceivedError(visitError) } } } @@ -768,12 +798,16 @@ class TurboSession internal constructor( super.onReceivedSslError(view, handler, error) handler.cancel() - logEvent("onReceivedSslError", "url" to error.url) + val visitError = TurboVisitError( + type = TurboVisitErrorType.WEB_SSL_ERROR, + code = error.primaryError + ) + + logEvent("onReceivedSslError", "error" to visitError) reset() - callback { it.onReceivedError(-1) } + callback { it.onReceivedError(visitError) } } - @TargetApi(Build.VERSION_CODES.O) override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { logEvent("onRenderProcessGone", "didCrash" to detail.didCrash()) diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt index c3940d81..5039600e 100644 --- a/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/session/TurboSessionCallback.kt @@ -2,17 +2,19 @@ package dev.hotwire.turbo.session import android.webkit.HttpAuthHandler import dev.hotwire.turbo.nav.TurboNavDestination +import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.visit.TurboVisitErrorType import dev.hotwire.turbo.visit.TurboVisitOptions internal interface TurboSessionCallback { fun onPageStarted(location: String) fun onPageFinished(location: String) - fun onReceivedError(errorCode: Int) + fun onReceivedError(error: TurboVisitError) fun onRenderProcessGone() fun onZoomed(newScale: Float) fun onZoomReset(newScale: Float) fun pageInvalidated() - fun requestFailedWithStatusCode(visitHasCachedSnapshot: Boolean, statusCode: Int) + fun requestFailedWithError(visitHasCachedSnapshot: Boolean, error: TurboVisitError) fun onReceivedHttpAuthRequest(handler: HttpAuthHandler, host: String, realm: String) fun visitRendered() fun visitCompleted(completedOffline: Boolean) diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt new file mode 100644 index 00000000..b7fe7539 --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitError.kt @@ -0,0 +1,18 @@ +package dev.hotwire.turbo.visit + +data class TurboVisitError( + /** + * + */ + val type: TurboVisitErrorType, + + /** + * + */ + val code: Int, + + /** + * + */ + val description: String? = null +) diff --git a/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitErrorType.kt b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitErrorType.kt new file mode 100644 index 00000000..be03f125 --- /dev/null +++ b/turbo/src/main/kotlin/dev/hotwire/turbo/visit/TurboVisitErrorType.kt @@ -0,0 +1,23 @@ +package dev.hotwire.turbo.visit + +enum class TurboVisitErrorType { + /** + * + */ + LOAD_ERROR, + + /** + * + */ + HTTP_ERROR, + + /** + * + */ + WEB_RESOURCE_ERROR, + + /** + * + */ + WEB_SSL_ERROR +} diff --git a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt index ce7a1b83..3ef66738 100644 --- a/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt +++ b/turbo/src/test/kotlin/dev/hotwire/turbo/session/TurboSessionTest.kt @@ -9,6 +9,8 @@ import dev.hotwire.turbo.nav.TurboNavDestination import dev.hotwire.turbo.util.toJson import dev.hotwire.turbo.views.TurboWebView import dev.hotwire.turbo.visit.TurboVisit +import dev.hotwire.turbo.visit.TurboVisitError +import dev.hotwire.turbo.visit.TurboVisitErrorType import dev.hotwire.turbo.visit.TurboVisitOptions import org.assertj.core.api.Assertions.assertThat import org.junit.Before @@ -88,6 +90,20 @@ class TurboSessionTest { assertThat(session.currentVisit?.identifier).isEqualTo(visitIdentifier) } + @Test + fun visitFailedToLoadCallsAdapter() { + val visitIdentifier = "12345" + + session.currentVisit = visit.copy(identifier = visitIdentifier) + session.turboFailedToLoad() + + verify(callback).onReceivedError(TurboVisitError( + type = TurboVisitErrorType.LOAD_ERROR, + code = -1, + description = "Turbo failed to load" + )) + } + @Test fun visitRequestFailedWithStatusCodeCallsAdapter() { val visitIdentifier = "12345" @@ -95,7 +111,11 @@ class TurboSessionTest { session.currentVisit = visit.copy(identifier = visitIdentifier) session.visitRequestFailedWithStatusCode(visitIdentifier, true, 500) - verify(callback).requestFailedWithStatusCode(true, 500) + verify(callback).requestFailedWithError(true, TurboVisitError( + type = TurboVisitErrorType.HTTP_ERROR, + code = 500, + description = "Request failed" + )) } @Test