From 72ee1d3774679e93922618bafbad9dce2524db69 Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Thu, 2 Jul 2026 18:34:14 -0500 Subject: [PATCH 1/2] feat(accountsetup): add iCloud app-password/2FA help in onboarding iCloud's guided setup screen previously linked only a generic Apple ID sign-in page and had no two-factor help link, unlike Gmail. Apple also requires two-factor authentication before it will issue an app-specific password, so: - MailProvider.ICLOUD.appPasswordHelpUrl now points at Apple's actual app-specific-password instructions (support.apple.com/en-us/102654) instead of the generic appleid.apple.com landing page. - MailProvider.ICLOUD.twoFactorHelpUrl now points at Apple's dedicated two-factor-authentication article (support.apple.com/en-us/102660), so the existing generic 2FA-help button in AppPasswordSetupScreen picks it up automatically, positioned the same as Gmail's (#152). - The 2FA button now reads "How to turn on Two-Factor Authentication" for iCloud instead of Google's "2-Step Verification" wording, via a new app_password_2fa_help_icloud string. Closes #153 Co-Authored-By: Claude Opus 4.8 --- .../ui/onboarding/OnboardingFlowTest.kt | 28 +++++++++++++++++-- .../libremail/domain/model/MailProvider.kt | 11 ++++++-- .../ui/accountsetup/AppPasswordSetupScreen.kt | 19 ++++++++++--- app/src/main/res/values/strings.xml | 1 + .../domain/model/MailProviderTest.kt | 15 ++++++++-- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/app/src/androidTest/kotlin/org/libremail/ui/onboarding/OnboardingFlowTest.kt b/app/src/androidTest/kotlin/org/libremail/ui/onboarding/OnboardingFlowTest.kt index 5308266..6ea9cca 100644 --- a/app/src/androidTest/kotlin/org/libremail/ui/onboarding/OnboardingFlowTest.kt +++ b/app/src/androidTest/kotlin/org/libremail/ui/onboarding/OnboardingFlowTest.kt @@ -208,8 +208,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). + // Gmail requires 2-Step Verification before app passwords, so its screen links Google's + // setup article (issue #98). iCloud gets the same kind of link, in Apple's own terminology + // (see icloudSetup_hasTwoFactorHelpLink); Yahoo does not (see + // yahooSetup_hasNoTwoFactorHelpLink). composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help)) .performScrollTo().assertIsDisplayed() composeTestRule.onNodeWithText(string(R.string.app_password_email)) @@ -238,7 +240,27 @@ class OnboardingFlowTest { // 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). + // …but gains no two-factor help link: unlike Gmail and iCloud, Yahoo gates nothing on it + // (issue #98, #153). composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help)).assertDoesNotExist() + composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help_icloud)).assertDoesNotExist() + } + + @Test + fun icloudSetup_hasTwoFactorHelpLink() { + setOnboardingContent(FakeAccountRepository(), FakeMailRepository()) + + composeTestRule.onNodeWithText(string(R.string.onboarding_add_account)).performClick() + waitForText("iCloud Mail") + composeTestRule.onNodeWithText("iCloud Mail").performClick() + + // Apple also won't issue an app-specific password until two-factor authentication is on, + // so iCloud's screen links Apple's own setup article too — using Apple's terminology for + // the button ("Two-Factor Authentication"), not Google's "2-Step Verification" (issue #153). + waitForText(string(R.string.app_password_2fa_help_icloud)) + composeTestRule.onNodeWithText(string(R.string.app_password_2fa_help_icloud)) + .performScrollTo().assertIsDisplayed() + composeTestRule.onNodeWithText(string(R.string.app_password_open_page, "iCloud Mail")) + .assertIsDisplayed() } } diff --git a/app/src/main/kotlin/org/libremail/domain/model/MailProvider.kt b/app/src/main/kotlin/org/libremail/domain/model/MailProvider.kt index 9d9ff26..5f93cc2 100644 --- a/app/src/main/kotlin/org/libremail/domain/model/MailProvider.kt +++ b/app/src/main/kotlin/org/libremail/domain/model/MailProvider.kt @@ -31,8 +31,8 @@ enum class MailProvider( 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. + * Gmail and iCloud both refuse to issue app passwords until two-factor auth is on, so both + * link their own setup article; Yahoo gates nothing on it. */ val twoFactorHelpUrl: String? = null, private val imapHost: String, @@ -74,7 +74,12 @@ enum class MailProvider( ICLOUD( key = "icloud", displayName = "iCloud Mail", - appPasswordHelpUrl = "https://appleid.apple.com", + // Apple's own app-specific-password instructions, not just the generic Apple ID sign-in + // page — this article also states the two-factor prerequisite below. + appPasswordHelpUrl = "https://support.apple.com/en-us/102654", + // Like Gmail, Apple won't issue an app-specific password until two-factor authentication + // is on, so link Apple's dedicated setup article too (issue #153). + twoFactorHelpUrl = "https://support.apple.com/en-us/102660", imapHost = "imap.mail.me.com", smtpHost = "smtp.mail.me.com", // Apple documents smtp.mail.me.com:587 with STARTTLS for iCloud Mail. diff --git a/app/src/main/kotlin/org/libremail/ui/accountsetup/AppPasswordSetupScreen.kt b/app/src/main/kotlin/org/libremail/ui/accountsetup/AppPasswordSetupScreen.kt index 357d4fe..2482252 100644 --- a/app/src/main/kotlin/org/libremail/ui/accountsetup/AppPasswordSetupScreen.kt +++ b/app/src/main/kotlin/org/libremail/ui/accountsetup/AppPasswordSetupScreen.kt @@ -151,15 +151,16 @@ fun AppPasswordSetupScreen( ) Spacer(Modifier.height(12.dp)) - // Only Gmail has a two-factor prerequisite (see MailProvider.twoFactorHelpUrl): its - // app-passwords page rejects accounts without 2-Step Verification, so point users - // there first instead of sending them to the app-passwords page's dead end. + // Gmail and iCloud both have a two-factor prerequisite (see + // MailProvider.twoFactorHelpUrl): neither will issue an app password until it's on, so + // point users there first instead of sending them to the app-passwords page's dead + // end. Yahoo has no twoFactorHelpUrl, so this renders nothing for it. provider.twoFactorHelpUrl?.let { twoFactorHelpUrl -> OutlinedButton( onClick = { openUrl(twoFactorHelpUrl) }, modifier = Modifier.fillMaxWidth(), ) { - Text(stringResource(R.string.app_password_2fa_help)) + Text(stringResource(twoFactorHelpLabel(provider))) } Spacer(Modifier.height(8.dp)) } @@ -269,6 +270,16 @@ private fun providerIntro(provider: MailProvider): Int = when (provider) { MailProvider.ICLOUD -> R.string.app_password_intro_icloud } +/** + * Button copy for the two-factor prerequisite link, in each provider's own terminology — Google + * calls it "2-Step Verification", Apple "Two-Factor Authentication". Only reached for providers + * that expose [MailProvider.twoFactorHelpUrl]; Yahoo has none, so its branch here is unused. + */ +private fun twoFactorHelpLabel(provider: MailProvider): Int = when (provider) { + MailProvider.ICLOUD -> R.string.app_password_2fa_help_icloud + MailProvider.GMAIL, MailProvider.YAHOO -> R.string.app_password_2fa_help +} + private fun MailSecurity.label(): String = when (this) { MailSecurity.SSL_TLS -> "SSL/TLS" MailSecurity.STARTTLS -> "STARTTLS" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9001283..26e026e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -184,6 +184,7 @@ Store this app password carefully — it grants full access to your email. LibreMail keeps it only on this device. Create an app password for %1$s How to turn on 2-Step Verification + How to turn on Two-Factor Authentication Couldn\'t open your browser Email address App password diff --git a/app/src/test/kotlin/org/libremail/domain/model/MailProviderTest.kt b/app/src/test/kotlin/org/libremail/domain/model/MailProviderTest.kt index 5ddfa07..69d16de 100644 --- a/app/src/test/kotlin/org/libremail/domain/model/MailProviderTest.kt +++ b/app/src/test/kotlin/org/libremail/domain/model/MailProviderTest.kt @@ -82,7 +82,7 @@ class MailProviderTest { } @Test - fun `only gmail links two-factor setup help, over https`() { + fun `gmail and icloud link two-factor setup help over https, yahoo does not`() { // 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( @@ -91,9 +91,18 @@ class MailProviderTest { ) 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. + // Apple also won't issue an app-specific password until two-factor authentication is on, + // so iCloud needs the same escape hatch — pointed at Apple's dedicated article, not a + // generic Apple ID sign-in page (issue #153). + assertEquals("https://support.apple.com/en-us/102660", MailProvider.ICLOUD.twoFactorHelpUrl) + + // Yahoo gates nothing on two-factor, so it must not grow the extra link. assertNull(MailProvider.YAHOO.twoFactorHelpUrl) - assertNull(MailProvider.ICLOUD.twoFactorHelpUrl) + } + + @Test + fun `icloud app-password help points at Apple's specific instructions, not the generic sign-in page`() { + assertEquals("https://support.apple.com/en-us/102654", MailProvider.ICLOUD.appPasswordHelpUrl) } @Test From 3c2bd0b1381908c22a2f7d887407e222b0779dbf Mon Sep 17 00:00:00 2001 From: Jason Ross <51939451+JMR-dev@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:45:39 -0500 Subject: [PATCH 2/2] ci: run autoupdate in the CI_CD environment so it can read AUTOUPDATE_TOKEN (#182) Co-authored-by: Claude Opus 4.8 --- .github/workflows/autoupdate.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/autoupdate.yml b/.github/workflows/autoupdate.yml index 855f3ee..c570043 100644 --- a/.github/workflows/autoupdate.yml +++ b/.github/workflows/autoupdate.yml @@ -30,6 +30,7 @@ jobs: autoupdate: name: Auto-update armed PRs runs-on: ubuntu-latest + environment: CI_CD steps: - name: Update behind PRs that have auto-merge enabled uses: chinthakagodawita/autoupdate@0707656cd062a3b0cf8fa9b2cda1d1404d74437e # v1.7.0