sync: a device with no browser no longer strands the flow
Both launches can fail, and the outer catch named only ActivityNotFoundException — so a SecurityException from a locked-down profile escaped the LaunchedEffect and took the app down. The failure was also ignored: onBrowserLaunched cleared openInBrowser regardless, so the user sat on "waiting for your browser" with a spinner and no browser for the rest of the twenty-minute window, on exactly the AOSP and GrapheneOS devices the Custom Tabs fallback exists for. A failed launch now says so, and the step offers the password path, which is the only way forward on such a device.
This commit is contained in:
@@ -42,6 +42,9 @@ sealed interface AddAccountMessage {
|
|||||||
data object BrowserMaintenance : Problem
|
data object BrowserMaintenance : Problem
|
||||||
data object BrowserFailed : Problem
|
data object BrowserFailed : Problem
|
||||||
|
|
||||||
|
/** No browser could be opened at all — AOSP, GrapheneOS, a locked-down profile. */
|
||||||
|
data object BrowserUnavailable : Problem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A home set on a host the credential is not scoped to.
|
* A home set on a host the credential is not scoped to.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package de.jeanlucmakiola.agendula.ui.accounts
|
package de.jeanlucmakiola.agendula.ui.accounts
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -96,12 +95,19 @@ internal fun AddAccountScreen(
|
|||||||
LaunchedEffect(state.openInBrowser) {
|
LaunchedEffect(state.openInBrowser) {
|
||||||
val url = state.openInBrowser ?: return@LaunchedEffect
|
val url = state.openInBrowser ?: return@LaunchedEffect
|
||||||
val uri = url.toString().toUri()
|
val uri = url.toString().toUri()
|
||||||
try {
|
// ⚠️ Neither failure may escape, and neither may be ignored. An
|
||||||
|
// exception out of a LaunchedEffect takes the app down — and the outer
|
||||||
|
// catch named only ActivityNotFoundException, so a SecurityException
|
||||||
|
// from a locked-down profile did exactly that. Reporting the failure is
|
||||||
|
// the other half: clearing openInBrowser regardless left the user on
|
||||||
|
// "waiting for your browser" with a spinner and no browser, on the very
|
||||||
|
// devices this fallback exists for.
|
||||||
|
val launched = runCatching {
|
||||||
CustomTabsIntent.Builder().build().launchUrl(context, uri)
|
CustomTabsIntent.Builder().build().launchUrl(context, uri)
|
||||||
} catch (_: ActivityNotFoundException) {
|
}.recoverCatching {
|
||||||
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, uri)) }
|
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||||
}
|
}.isSuccess
|
||||||
viewModel.onBrowserLaunched()
|
if (launched) viewModel.onBrowserLaunched() else viewModel.onBrowserUnavailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backing out abandons a flow that may still be polling the server every two
|
// Backing out abandons a flow that may still be polling the server every two
|
||||||
@@ -349,6 +355,8 @@ private fun AddAccountMessage.text(): String = when (this) {
|
|||||||
stringResource(R.string.add_account_browser_error_maintenance)
|
stringResource(R.string.add_account_browser_error_maintenance)
|
||||||
AddAccountMessage.BrowserFailed ->
|
AddAccountMessage.BrowserFailed ->
|
||||||
stringResource(R.string.add_account_browser_error_failed)
|
stringResource(R.string.add_account_browser_error_failed)
|
||||||
|
AddAccountMessage.BrowserUnavailable ->
|
||||||
|
stringResource(R.string.add_account_browser_error_unavailable)
|
||||||
is AddAccountMessage.OutsideCredentialScope ->
|
is AddAccountMessage.OutsideCredentialScope ->
|
||||||
stringResource(R.string.add_account_error_cross_domain, host)
|
stringResource(R.string.add_account_error_cross_domain, host)
|
||||||
is AddAccountMessage.Quirk -> when (quirk) {
|
is AddAccountMessage.Quirk -> when (quirk) {
|
||||||
|
|||||||
@@ -392,6 +392,21 @@ class AddAccountViewModel @Inject constructor(
|
|||||||
|
|
||||||
fun onBrowserLaunched() = _state.update { it.copy(openInBrowser = null) }
|
fun onBrowserLaunched() = _state.update { it.copy(openInBrowser = null) }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nothing could open the URL, so there is no approval coming.
|
||||||
|
*
|
||||||
|
* ⚠️ Without this, a failed launch still cleared `openInBrowser` and left the
|
||||||
|
* user watching "waiting for your browser" for the rest of a twenty-minute
|
||||||
|
* window, on exactly the browserless devices the Custom Tabs fallback exists
|
||||||
|
* for. Nothing was minted — the flow was never opened — so there is nothing
|
||||||
|
* to hand back; the poll is stopped because it is polling for an approval
|
||||||
|
* that cannot happen.
|
||||||
|
*/
|
||||||
|
fun onBrowserUnavailable() {
|
||||||
|
pollJob?.cancel()
|
||||||
|
browserFailed(AddAccountMessage.BrowserUnavailable)
|
||||||
|
}
|
||||||
|
|
||||||
fun onListToggled(url: HttpUrl) {
|
fun onListToggled(url: HttpUrl) {
|
||||||
val step = _state.value.step as? AddAccountStep.ChooseLists ?: return
|
val step = _state.value.step as? AddAccountStep.ChooseLists ?: return
|
||||||
val selected = if (url in step.selected) step.selected - url else step.selected + url
|
val selected = if (url in step.selected) step.selected - url else step.selected + url
|
||||||
|
|||||||
@@ -348,6 +348,7 @@
|
|||||||
<string name="add_account_browser_error_rate_limited">The server is turning away repeated attempts. Wait a few minutes and try again.</string>
|
<string name="add_account_browser_error_rate_limited">The server is turning away repeated attempts. Wait a few minutes and try again.</string>
|
||||||
<string name="add_account_browser_error_maintenance">The server is in maintenance mode. Try again once it is back.</string>
|
<string name="add_account_browser_error_maintenance">The server is in maintenance mode. Try again once it is back.</string>
|
||||||
<string name="add_account_browser_error_failed">The server didn\u2019t finish signing you in. Try again in a moment.</string>
|
<string name="add_account_browser_error_failed">The server didn\u2019t finish signing you in. Try again in a moment.</string>
|
||||||
|
<string name="add_account_browser_error_unavailable">This device has no browser that can open the sign-in page. Use a password instead.</string>
|
||||||
<string name="add_account_quirk_hint_fastmail">Fastmail needs an app password, not your account password \u2014 and CalDAV is not on the Basic plan.</string>
|
<string name="add_account_quirk_hint_fastmail">Fastmail needs an app password, not your account password \u2014 and CalDAV is not on the Basic plan.</string>
|
||||||
<string name="add_account_quirk_hint_icloud">iCloud needs an app-specific password, which you create at appleid.apple.com with two-factor on.</string>
|
<string name="add_account_quirk_hint_icloud">iCloud needs an app-specific password, which you create at appleid.apple.com with two-factor on.</string>
|
||||||
<string name="add_account_quirk_fastmail">Fastmail needs an app password, and CalDAV is not available on the Basic plan.</string>
|
<string name="add_account_quirk_fastmail">Fastmail needs an app password, and CalDAV is not available on the Basic plan.</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user