Skip to content

Commit

Permalink
[go] Update incompatible version screen with links to expo.dev/go (#…
Browse files Browse the repository at this point in the history
…27918)

# Why

We want to link people to `https://expo.dev/go` when they launch an
incompatible project in Expo Go so it's easier for them to install a
compatible Expo Go app.

ENG-11732

# How

Edited the existing messages to include installation links. Depending if
running on a iOS device or in a simulator the behaviour is slightly
different.

Also updated the code to work correctly when only one SDK version is
available, on iOS added a more universal link parser that changes any
combination of `[link](description)` into a hyperlink instead.

# Test Plan

Tested in Expo Go on a physical Android device, emulator, physical iOS
device and iOS emulator

|Android|iOS Simulator|iOS Device|
| --- | --- | --- |
|<img width="200" alt="android"
src="https://github.com/expo/expo/assets/31368152/bbcbeb16-af72-4d04-a078-bc4185093fdc">
| <img width="200" alt="simulator"
src="https://github.com/expo/expo/assets/31368152/676296fa-e867-4153-b01a-690eb9a36664">|
<img width="200" alt="physical"
src="https://github.com/expo/expo/assets/31368152/9bbfa919-a052-4bd8-9b52-8fbce07a5042">|
  • Loading branch information
behenate committed Apr 19, 2024
1 parent c241cb1 commit ca7ee4b
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 139 deletions.
12 changes: 5 additions & 7 deletions apps/expo-go/android/app/src/main/res/layout/error_fragment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:layout_above="@+id/view_error_log"
android:gravity="center"
android:orientation="vertical">

<TextView
android:id="@+id/error_header"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginLeft="20dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="30dp"
android:layout_marginRight="20dp"
android:gravity="center"
android:layout_marginRight="15dp"
android:text="@string/error_header"
android:textColor="@color/white"
android:textSize="24sp"/>
Expand All @@ -38,9 +37,8 @@
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_marginTop="20dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:gravity="center"
android:layout_marginLeft="15dp"
android:layout_marginRight="15dp"
android:text="@string/error_default_client"
android:textColor="@color/white"
android:textSize="16sp"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ object ExceptionUtils {
return null
}

fun exceptionToCanRetry(exception: Exception): Boolean {
if (exception is ManifestException) {
return exception.canRetry
}
return true
}

private fun getUserErrorMessage(exception: Exception?, context: Context): String? {
if (exception is UnknownHostException || exception is ConnectException) {
if (isAirplaneModeOn(context)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,15 @@ import org.json.JSONException
import org.json.JSONObject
import java.lang.Exception
import java.lang.NumberFormatException
import expo.modules.core.utilities.EmulatorUtilities.isRunningOnEmulator

class ManifestException : ExponentException {
private val manifestUrl: String
private var errorJSON: JSONObject? = null
private lateinit var errorMessage: String
private var fixInstructions: String? = null

var canRetry: Boolean = true
val errorHeader: String?
get() = errorJSON?.let {
try {
Expand All @@ -29,106 +34,137 @@ class ManifestException : ExponentException {
constructor(originalException: Exception?, manifestUrl: String) : super(originalException) {
this.manifestUrl = manifestUrl
this.errorJSON = null
processException()
}

constructor(originalException: Exception?, manifestUrl: String, errorJSON: JSONObject?) : super(
originalException
) {
this.errorJSON = errorJSON
this.manifestUrl = manifestUrl
processException()
}

override fun toString(): String {
private fun processException() {
val extraMessage = if (ExpoViewBuildConfig.DEBUG) {
" Are you sure expo-cli is running?"
} else {
""
}

return when (manifestUrl) {
else -> {
var formattedMessage = "Could not load $manifestUrl.$extraMessage"
val supportedSdks = Constants.SDK_VERSIONS_LIST.map {
it.substring(0, it.indexOf('.')).toInt()
}.sorted()
val supportedSdksString = { conjunction: String ->
supportedSdks.subList(0, supportedSdks.size - 1)
.joinToString(", ") + " $conjunction ${supportedSdks.last()}"
}
var formattedMessage = "Could not load $manifestUrl.$extraMessage"
val supportedSdks = Constants.SDK_VERSIONS_LIST.map {
it.substring(0, it.indexOf('.')).toInt()
}.sorted()
val supportedSdksString = { conjunction: String ->
if (supportedSdks.size == 1) {
supportedSdks[0]
} else {
supportedSdks.subList(0, supportedSdks.size - 1)
.joinToString(", ") + " $conjunction ${supportedSdks.last()}"
}
}

errorJSON?.let { errorJSON ->
try {
val errorCode = errorJSON.getString("errorCode")
val rawMessage = errorJSON.getString("message")
when (errorCode) {
"EXPERIENCE_NOT_FOUND", // Really doesn't exist
"EXPERIENCE_NOT_PUBLISHED_ERROR", // Not published
"EXPERIENCE_RELEASE_NOT_FOUND_ERROR" -> // Can't find a release for the requested release channel
formattedMessage = "No project found at $manifestUrl."

if (errorJSON != null) {
try {
val errorCode = errorJSON!!.getString("errorCode")
val rawMessage = errorJSON!!.getString("message")
when (errorCode) {
"EXPERIENCE_NOT_FOUND", // Really doesn't exist
"EXPERIENCE_NOT_PUBLISHED_ERROR", // Not published
"EXPERIENCE_RELEASE_NOT_FOUND_ERROR" -> // Can't find a release for the requested release channel
formattedMessage =
"No project found at $manifestUrl."
"EXPERIENCE_SDK_VERSION_OUTDATED" -> {
val metadata = errorJSON!!.getJSONObject("metadata")
val availableSDKVersions = metadata.getJSONArray("availableSDKVersions")
val sdkVersionRequired = availableSDKVersions.getString(0).let {
it.substring(0, it.indexOf('.'))
}

formattedMessage =
"This project uses SDK $sdkVersionRequired, but this version of Expo Go supports only SDKs ${supportedSdksString("and")}.<br><br>" +
"To open this project:<br>" +
"• Update it to SDK ${supportedSdksString("or")}.<br>" +
"• Install an older version of Expo Go that supports the project's SDK version.<br><br>" +
"If you are unsure how to update the project or install a suitable version of Expo Go, refer to the <a href='https://docs.expo.dev/get-started/expo-go/#sdk-versions'>SDK Versions Guide</a>."
}
"SNACK_NOT_FOUND_FOR_SDK_VERSION" -> {
formattedMessage =
"Incompatible SDK version or no SDK version specified. This version of Expo Go only supports the following SDKs (runtimes): " + Constants.SDK_VERSIONS_LIST.joinToString() + ". A development build must be used to load other runtimes.<br><a href='https://docs.expo.dev/develop/development-builds/introduction/'>Learn more about development builds</a>."
}
"EXPERIENCE_SDK_VERSION_TOO_NEW" ->
formattedMessage =
"This project requires a newer version of Expo Go - please download the latest version from the Play Store."
"EXPERIENCE_NOT_VIEWABLE" ->
formattedMessage =
rawMessage // From server: The experience you requested is not viewable by you. You will need to log in or ask the owner to grant you access.
"USER_SNACK_NOT_FOUND", "SNACK_NOT_FOUND" ->
formattedMessage =
"No snack found at $manifestUrl."
"SNACK_RUNTIME_NOT_RELEASED" ->
formattedMessage =
rawMessage // From server: `The Snack runtime for corresponding sdk version of this Snack ("${sdkVersions[0]}") is not released.`,
"SNACK_NOT_FOUND_FOR_SDK_VERSION" -> run closure@{
val metadata = errorJSON!!.getJSONObject("metadata")
val fullName = metadata["fullName"] ?: ""
val snackSdkVersion =
(metadata["sdkVersions"] as? JSONArray)?.get(0) as? String ?: "unknown"

if (snackSdkVersion == "unknown" || snackSdkVersion.indexOf(".") == -1) {
formattedMessage = rawMessage
return@closure
}

val snackSdkVersionValue = try {
Integer.parseInt(snackSdkVersion.substring(0, snackSdkVersion.indexOf(".")))
} catch (e: NumberFormatException) {
formattedMessage = rawMessage
return@closure
}
formattedMessage =
"The snack \"${fullName}\" was found, but it is not compatible with your version of Expo Go. It was released for SDK $snackSdkVersionValue, but your Expo Go supports only SDKs ${supportedSdksString("and")}."
formattedMessage += if (supportedSdks.last() < snackSdkVersionValue) {
"<br><br>You need to update your Expo Go app in order to run this Snack."
} else {
"<br><br>Snack needs to be upgraded to a current SDK version. To do it, open the project at <a href='https://snack.expo.dev'>Expo Snack website</a>. It will be automatically upgraded to a supported SDK version."
}
formattedMessage += "<br><br><a href='https://docs.expo.dev/get-started/expo-go/#sdk-versions'>Learn more about SDK versions and Expo Go</a>."
}
"EXPERIENCE_SDK_VERSION_OUTDATED" -> {
val metadata = errorJSON.getJSONObject("metadata")
val availableSDKVersions = metadata.getJSONArray("availableSDKVersions")
val sdkVersionRequired = availableSDKVersions.getString(0).let {
it.substring(0, it.indexOf('.'))
}
} catch (e: JSONException) {
return formattedMessage
val maybePluralSDKsString = "SDK${"s".takeIf { supportedSdks.size > 1 } ?: ""}"
val expoDevLink =
"https://expo.dev/go?sdkVersion=$sdkVersionRequired&platform=android&device=${!isRunningOnEmulator()}"

formattedMessage =
"• The installed version of Expo Go is for <b>$maybePluralSDKsString ${
supportedSdksString(
"and"
)
}</b>.<br>" +
"• The project you opened uses <b>SDK $sdkVersionRequired</b>."
fixInstructions =
"Either upgrade this project to SDK ${supportedSdksString("or")} or install an older version of Expo Go that is compatible with your project.<br><br>" +
"<a href='https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/'>Learn how to upgrade to SDK ${supportedSdks.last()}.</a><br><br>" +
"<a href='$expoDevLink'>Learn how to install Expo Go for SDK $sdkVersionRequired</a>."
canRetry = false
}

"EXPERIENCE_SDK_VERSION_TOO_NEW" -> {
formattedMessage = "This project requires a newer version of Expo Go."
fixInstructions = "Download the latest version of Expo Go from the Play Store."
canRetry = false
}

"EXPERIENCE_NOT_VIEWABLE" -> {
formattedMessage = "The experience you requested is not viewable by you."
fixInstructions =
"You need to log in. If the snack is still unavailable after logging in, ask the owner to grant you access."
}

"USER_SNACK_NOT_FOUND", "SNACK_NOT_FOUND" ->
formattedMessage = "No snack found at $manifestUrl."

"SNACK_RUNTIME_NOT_RELEASED" ->
formattedMessage =
rawMessage // From server: `The Snack runtime for corresponding sdk version of this Snack ("${sdkVersions[0]}") is not released.`,

"SNACK_NOT_FOUND_FOR_SDK_VERSION" -> run closure@{
val metadata = errorJSON.getJSONObject("metadata")
val fullName = metadata["fullName"] ?: ""
val snackSdkVersion =
(metadata["sdkVersions"] as? JSONArray)?.get(0) as? String ?: "unknown"
val maybePluralSDKsString = "SDK${"s".takeIf { supportedSdks.size > 1 } ?: ""}"

if (snackSdkVersion == "unknown" || snackSdkVersion.indexOf(".") == -1) {
formattedMessage = rawMessage
return@closure
}

val snackSdkVersionValue = try {
Integer.parseInt(snackSdkVersion.substring(0, snackSdkVersion.indexOf(".")))
} catch (e: NumberFormatException) {
formattedMessage = rawMessage
return@closure
}
formattedMessage =
"The snack \"${fullName}\" was found, but it is not compatible with your version of Expo Go. It was released for SDK $snackSdkVersionValue, but your Expo Go supports only $maybePluralSDKsString ${
supportedSdksString(
"and"
)
}."

fixInstructions = if (supportedSdks.last() < snackSdkVersionValue) {
"You need to update your Expo Go app in order to run this Snack."
} else {
"Snack needs to be upgraded to a current SDK version. To do it, open the project at <a href='https://snack.expo.dev'>Expo Snack website</a>. It will be automatically upgraded to a supported SDK version."
}
fixInstructions += "<br><br><a href='https://docs.expo.dev/get-started/expo-go/#sdk-versions'>Learn more about SDK versions and Expo Go</a>."
canRetry = false
}
}
formattedMessage
} catch (e: JSONException) {
errorMessage = formattedMessage
fixInstructions = null
canRetry = true
}
}
errorMessage = formattedMessage
}

override fun toString(): String {
if (fixInstructions != null) {
return "$errorMessage<br><br><h5><b>How to fix this error</b></h5>$fixInstructions"
}
return errorMessage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import host.exp.exponent.storage.ExponentSharedPreferences
import host.exp.expoview.Exponent
import javax.inject.Inject

data class ErrorProcessingResult(
val isFatal: Boolean,
val errorMessage: ExponentErrorMessage,
val errorHeader: String?,
val canRetry: Boolean
)

abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
abstract class ExperienceEvent internal constructor(val experienceKey: ExperienceKey)

Expand Down Expand Up @@ -137,7 +144,7 @@ abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
return@runOnUiThread
}
kernel.exponentSharedPreferences.setLong(ExponentSharedPreferences.ExponentSharedPreferencesKey.LAST_FATAL_ERROR_DATE_KEY, System.currentTimeMillis())
val (isFatal, errorMessage, errorHeader) = sendErrorsToErrorActivity()
val (isFatal, errorMessage, errorHeader, canRetry) = sendErrorsToErrorActivity()
if (!shouldShowErrorScreen(errorMessage)) {
return@runOnUiThread
}
Expand All @@ -164,6 +171,7 @@ abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
ErrorActivity.DEVELOPER_ERROR_MESSAGE_KEY,
errorMessage.developerErrorMessage()
)
putExtra(ErrorActivity.CAN_RETRY_KEY, canRetry)
}
startActivity(intent)
EventBus.getDefault().post(ExperienceDoneLoadingEvent(this))
Expand Down Expand Up @@ -196,10 +204,11 @@ abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
// Otherwise onResume will consumeErrorQueue
}

private fun sendErrorsToErrorActivity(): Triple<Boolean, ExponentErrorMessage, String?> {
private fun sendErrorsToErrorActivity(): ErrorProcessingResult {
var isFatal = false
var errorMessage = developerErrorMessage("")
var errorHeader: String? = null
var canRetry = true
synchronized(errorQueue) {
while (!errorQueue.isEmpty()) {
val error = errorQueue.remove()
Expand All @@ -211,9 +220,10 @@ abstract class BaseExperienceActivity : MultipleVersionReactNativeActivity() {
if (error.isFatal) {
isFatal = true
}
canRetry = canRetry && error.canRetry
}
}
return Triple(isFatal, errorMessage, errorHeader)
return ErrorProcessingResult(isFatal, errorMessage, errorHeader, canRetry)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ class ErrorActivity() : FragmentActivity() {
const val DEVELOPER_ERROR_MESSAGE_KEY = "developerErrorMessage"
const val DEBUG_MODE_KEY = "isDebugModeEnabled"
const val ERROR_HEADER_KEY = "errorHeader"
const val CAN_RETRY_KEY = "canRetry"

@JvmStatic var visibleActivity: ErrorActivity? = null
private set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ErrorFragment : Fragment() {
val userErrorMessage = bundle.getString(ErrorActivity.USER_ERROR_MESSAGE_KEY)
val developerErrorMessage = bundle.getString(ErrorActivity.DEVELOPER_ERROR_MESSAGE_KEY)
val errorHeader = bundle.getString(ErrorActivity.ERROR_HEADER_KEY)
val canRetry = bundle.getBoolean(ErrorActivity.CAN_RETRY_KEY, true)

var defaultErrorMessage = userErrorMessage

Expand All @@ -74,6 +75,10 @@ class ErrorFragment : Fragment() {
binding.homeButton.visibility = View.GONE
}

if (!canRetry) {
binding.reloadButton.visibility = View.GONE
}

// Some errors are in HTML format and contain hyperlinks with instructions / more information (
// eq. EXPERIENCE_SDK_VERSION_OUTDATED). We detect HTML tags and render that text as HTML.
val htmlPattern = Pattern.compile("<([A-Za-z][A-Za-z0-9]*)\\b[^>]*>(.*?)</\\1>")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ class ExponentError(
val errorHeader: String?,
val stack: Array<Bundle>,
val exceptionId: Int,
val isFatal: Boolean
val isFatal: Boolean,
val canRetry: Boolean = true
) {
val timestamp: Date = Calendar.getInstance().time

Expand All @@ -22,6 +23,7 @@ class ExponentError(
put("errorMessage", errorMessage.developerErrorMessage())
put("exceptionId", exceptionId)
put("isFatal", isFatal)
put("canRetry", canRetry)
}
} catch (e: JSONException) {
null
Expand Down

0 comments on commit ca7ee4b

Please sign in to comment.