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 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.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package de.jeanlucmakiola.agendula.ui.accounts
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -96,12 +95,19 @@ internal fun AddAccountScreen(
|
||||
LaunchedEffect(state.openInBrowser) {
|
||||
val url = state.openInBrowser ?: return@LaunchedEffect
|
||||
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)
|
||||
} catch (_: ActivityNotFoundException) {
|
||||
runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, uri)) }
|
||||
}
|
||||
viewModel.onBrowserLaunched()
|
||||
}.recoverCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
}.isSuccess
|
||||
if (launched) viewModel.onBrowserLaunched() else viewModel.onBrowserUnavailable()
|
||||
}
|
||||
|
||||
// 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)
|
||||
AddAccountMessage.BrowserFailed ->
|
||||
stringResource(R.string.add_account_browser_error_failed)
|
||||
AddAccountMessage.BrowserUnavailable ->
|
||||
stringResource(R.string.add_account_browser_error_unavailable)
|
||||
is AddAccountMessage.OutsideCredentialScope ->
|
||||
stringResource(R.string.add_account_error_cross_domain, host)
|
||||
is AddAccountMessage.Quirk -> when (quirk) {
|
||||
|
||||
@@ -392,6 +392,21 @@ class AddAccountViewModel @Inject constructor(
|
||||
|
||||
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) {
|
||||
val step = _state.value.step as? AddAccountStep.ChooseLists ?: return
|
||||
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_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_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_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>
|
||||
|
||||
Reference in New Issue
Block a user