Merge pull request #115 from JMR-dev/feat-onboarding-2fa-link
feat(onboarding): link Google 2FA help from Gmail app-password step
This commit was merged in pull request #115.
This commit is contained in:
@@ -53,7 +53,7 @@ class OnboardingFlowTest {
|
||||
@get:Rule
|
||||
val composeTestRule = createAndroidComposeRule<ComponentActivity>()
|
||||
|
||||
private fun string(resId: Int) = composeTestRule.activity.getString(resId)
|
||||
private fun string(resId: Int, vararg args: Any) = composeTestRule.activity.getString(resId, *args)
|
||||
|
||||
// Generous cap for the slow, animation-disabled CI matrix emulators; waitUntil returns as soon
|
||||
// as the text appears, so the happy path is unaffected.
|
||||
@@ -206,6 +206,10 @@ class OnboardingFlowTest {
|
||||
// add" button sit below the fold of this scrolling screen, and a positional click on an
|
||||
// off-screen button is a silent no-op (which is why this passed only on API 37's taller AVD).
|
||||
waitForText(string(R.string.app_password_email))
|
||||
// Gmail requires 2-Step Verification before app passwords, so its screen (and only its
|
||||
// screen — see yahooSetup_hasNoTwoFactorHelpLink) links Google's setup article (issue #98).
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help))
|
||||
.performScrollTo().assertIsDisplayed()
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_email))
|
||||
.performScrollTo().performTextInput("e2e@gmail.com")
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_field))
|
||||
@@ -221,4 +225,18 @@ class OnboardingFlowTest {
|
||||
waitForText("E2E first message")
|
||||
composeTestRule.onNodeWithText("E2E first message").assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun yahooSetup_hasNoTwoFactorHelpLink() {
|
||||
setOnboardingContent(FakeAccountRepository(), FakeMailRepository())
|
||||
|
||||
composeTestRule.onNodeWithText(string(R.string.onboarding_add_account)).performClick()
|
||||
waitForText("Yahoo Mail")
|
||||
composeTestRule.onNodeWithText("Yahoo Mail").performClick()
|
||||
|
||||
// Yahoo's setup screen keeps its app-password link…
|
||||
waitForText(string(R.string.app_password_open_page, "Yahoo Mail"))
|
||||
// …but gains no 2-Step Verification link: that prerequisite is Gmail-specific (issue #98).
|
||||
composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help)).assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ enum class MailProvider(
|
||||
val displayName: String,
|
||||
/** The page where the user creates an app password for this provider. */
|
||||
val appPasswordHelpUrl: String,
|
||||
/**
|
||||
* Setup instructions for the provider's two-factor prerequisite, or null when there isn't one.
|
||||
* Only Gmail refuses to create app passwords until 2-Step Verification is on, so only Gmail
|
||||
* links its setup article; Yahoo and iCloud gate nothing on it.
|
||||
*/
|
||||
val twoFactorHelpUrl: String? = null,
|
||||
private val imapHost: String,
|
||||
private val smtpHost: String,
|
||||
private val smtpPort: Int,
|
||||
@@ -38,6 +44,9 @@ enum class MailProvider(
|
||||
key = "gmail",
|
||||
displayName = "Gmail",
|
||||
appPasswordHelpUrl = "https://myaccount.google.com/apppasswords",
|
||||
// Google's "Turn on 2-Step Verification" article — the app-passwords page above bounces
|
||||
// accounts that haven't enabled it yet, so the setup screen offers this as a way out.
|
||||
twoFactorHelpUrl = "https://support.google.com/accounts/answer/185839",
|
||||
imapHost = "imap.gmail.com",
|
||||
smtpHost = "smtp.gmail.com",
|
||||
// Google documents smtp.gmail.com:587 with STARTTLS as the standard submission endpoint.
|
||||
|
||||
@@ -78,6 +78,12 @@ fun AppPasswordSetupScreen(
|
||||
val scope = rememberCoroutineScope()
|
||||
// Resolved up front so the failure handler (a non-composable lambda) can use it.
|
||||
val openFailedMessage = stringResource(R.string.app_password_open_failed)
|
||||
// Shared by every outbound link on this screen: openUri throws if no browser/handler is
|
||||
// installed, so surface that as an inline snackbar instead of crashing.
|
||||
val openUrl: (String) -> Unit = { url ->
|
||||
runCatching { uriHandler.openUri(url) }
|
||||
.onFailure { scope.launch { snackbarHostState.showSnackbar(openFailedMessage) } }
|
||||
}
|
||||
|
||||
LaunchedEffect(form.status, form.addedAccountId) {
|
||||
if (form.status == SetupStatus.DONE) {
|
||||
@@ -146,15 +152,23 @@ fun AppPasswordSetupScreen(
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
// openUri throws if no browser/handler is installed; surface it instead of crashing.
|
||||
runCatching { uriHandler.openUri(provider.appPasswordHelpUrl) }
|
||||
.onFailure { scope.launch { snackbarHostState.showSnackbar(openFailedMessage) } }
|
||||
},
|
||||
onClick = { openUrl(provider.appPasswordHelpUrl) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.app_password_open_page, provider.displayName))
|
||||
}
|
||||
// Only Gmail has a two-factor prerequisite (see MailProvider.twoFactorHelpUrl): its
|
||||
// app-passwords page rejects accounts without 2-Step Verification, so give those users
|
||||
// a way to set it up instead of a dead end.
|
||||
provider.twoFactorHelpUrl?.let { twoFactorHelpUrl ->
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedButton(
|
||||
onClick = { openUrl(twoFactorHelpUrl) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.app_password_2fa_help))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
OutlinedTextField(
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
<string name="app_password_what_is">An app password is a one-off password that lets an app sign in to your account without your main password or a two-factor code.</string>
|
||||
<string name="app_password_warning">Store this app password carefully — it grants full access to your email. LibreMail keeps it only on this device.</string>
|
||||
<string name="app_password_open_page">Create an app password for %1$s</string>
|
||||
<string name="app_password_2fa_help">How to turn on 2-Step Verification</string>
|
||||
<string name="app_password_open_failed">Couldn\'t open your browser</string>
|
||||
<string name="app_password_email">Email address</string>
|
||||
<string name="app_password_field">App password</string>
|
||||
|
||||
@@ -80,6 +80,21 @@ class MailProviderTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only gmail links two-factor setup help, over https`() {
|
||||
// Gmail's app-passwords page rejects accounts without 2-Step Verification, so its setup
|
||||
// screen must offer the setup article as a way out (issue #98).
|
||||
val gmailUrl = assertNotNull(
|
||||
MailProvider.GMAIL.twoFactorHelpUrl,
|
||||
"gmail must expose a 2-Step Verification help URL",
|
||||
)
|
||||
assertTrue(gmailUrl.startsWith("https://"), "gmail 2FA help URL must be https")
|
||||
|
||||
// Yahoo and iCloud gate nothing on two-factor, so they must not grow the extra link.
|
||||
assertNull(MailProvider.YAHOO.twoFactorHelpUrl)
|
||||
assertNull(MailProvider.ICLOUD.twoFactorHelpUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createAccount trims the email and derives a stable id and display name`() {
|
||||
val account = MailProvider.GMAIL.createAccount(" User@Gmail.com ")
|
||||
|
||||
Reference in New Issue
Block a user