Skip to content

Fix false connectivity banner on private Atomic sites#22926

Merged
jkmassel merged 7 commits into
release/26.8from
fix/atomic-private-site-capability-detection
Jun 4, 2026
Merged

Fix false connectivity banner on private Atomic sites#22926
jkmassel merged 7 commits into
release/26.8from
fix/atomic-private-site-capability-detection

Conversation

@jkmassel

@jkmassel jkmassel commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a regression where the My Site "Unable to connect to your site" banner appears for private WP.com Atomic sites even though the site is fully reachable. Introduced by #22883 (present in both trunk and release/26.8).

There's a narrow case where this fix doesn't work:

  • An atomic site
  • That's private
  • That uses a plugin or configuration change to move the REST API root away from /wp-json

This is caused by #22905.

Root cause

The banner is driven by EditorSettingsRepository.fetchEditorCapabilitiesForSite(), which runs two probes and requires both to pass. For Atomic sites the route-support probe was changed in #22883 to run REST API autodiscovery against the site's own host instead of the WP.com proxy:

wpLoginClient.apiDiscovery(site.url)

WpLoginClient is the login-flow client — it's constructed with no credentials (ApplicationModule.provideWpLoginClient), so that discovery call is unauthenticated. A private Atomic site gates anonymous requests to its host, so discovery fails (FailureFetchAndParseApiRoot), the route probe returns false, and the banner renders — even though the proxy-backed theme probe in the same fetch succeeds.

Confirmed on device against a private Atomic site, where a single refresh shows the failure is specifically the unauthenticated probe:

Request Transport Auth Result
App-password validation direct host Basic auth ✅ Success
Theme fetch WP.com proxy bearer token ✅ Success
Capability discovery direct host none FailureFetchAndParseApiRoot

Fix

When direct-host discovery fails and the site has application-password credentials, fall back to an authenticated probe against the same direct host using the app-password (Basic auth) client — the transport the editor actually uses on Atomic. Routes are read from apiRoot().get() and resolved against the direct host, mirroring fetchRouteSupportViaConfiguredClient but bypassing the WP.com proxy. Public Atomic sites are unchanged (discovery still succeeds on the first try).

Verified on-device: after the fix the banner no longer appears on a private Atomic site — discovery still fails, the authenticated probe succeeds, and capability detection completes.

  • EditorSettingsRepository: fetchRouteSupportViaDirectHostDiscovery falls back to a new fetchRouteSupportViaApplicationPasswordClient when discovery fails and hasApplicationPasswordCredentials() is true.
  • WpApiClientProvider: adds getDirectHostApiUrlResolver(site) — the resolver counterpart to getApplicationPasswordClient, so routes fetched through that client resolve against the host they came from.

First-login hardening (from review)

The application password is minted asynchronously on the My Site screen, so on a fresh login the first capability fetch can race ahead of it. Three follow-on behaviours keep that from misfiring:

  • Pending ≠ failed. While the mint is in flight, a credential-less fetch on an Atomic site is treated as pending and the banner is suppressed, instead of flashing a false "can't connect" (EditorSettingsRepository.isAwaitingApplicationPassword).
  • Auto re-detect. A new ApplicationPasswordMonitor signals when credentials are established (headless mint or interactive login); the connectivity slice re-runs detection immediately, so capabilities settle without a manual pull-to-refresh.
  • Terminal failure still surfaces. If the mint fails terminally, the monitor records it and the banner is shown again. This matters because the application-password card can't render its auth prompt on a private host (its authorize-URL lookup uses the same gated, unauthenticated discovery), so without this the user would get no signal at all.

What we explored

  1. Authenticated fallback after discovery fails ✅ — chosen. Minimal behaviour change: public Atomic keeps the exact tested path; only the broken private case gains the authenticated retry.
  2. Prefer the app-password probe whenever credentials exist (skip discovery). Saves a round-trip on private sites but changes behaviour for public app-password sites and loses discovery's "don't assume /wp-json" robustness for the common case.
  3. Fall back to the WP.com proxy's route list. Reintroduces exactly the proxy/direct-host divergence Probe direct host for editor capability detection on Atomic sites #22883 fixed (Editor fails to load on Atomic sites when theme-styles capability over-reports availability #22879).

Out of scope

Giving private Atomic sites a specific "set up access to your site" prompt — rather than the generic connectivity banner — on terminal mint failure. The natural owner, the application-password card, can't render on a private host today because its authorize-URL lookup relies on the same unauthenticated discovery the gated host rejects; fixing that is a separate follow-up.

Testing instructions

First login (the banner race):

  1. Sign out, then sign into a WP.com account with a private Atomic site that has an application password.
  2. Open My Site for that site and let the dashboard settle — do not pull to refresh.
  • Verify the "Unable to connect to your site" banner does not flash, and capabilities settle on their own.

Private Atomic site (the regression):

  1. Sign into a WP.com account with a private Atomic site that has an application password.
  2. Open My Site, pull to refresh, let the dashboard settle.
  • Verify the "Unable to connect to your site" banner does not appear.
  1. Create a new post.
  • Verify the editor opens correctly.

Public Atomic site (regression check):

  1. Select a public Atomic site (e.g. automatticwidgets.wpcomstaging.com).
  2. Open My Site, pull to refresh.
  • Verify no banner and capability detection still completes.

Simple site (regression check):

  1. Select a WP.com Simple site, open My Site, pull to refresh.
  • Verify behaviour is unchanged.

Unit tests:

  • ./gradlew :WordPress:testJetpackDebugUnitTest across EditorSettingsRepositoryTest, SiteConnectivityBannerViewModelSliceTest, ApplicationPasswordViewModelSliceTest, and ApplicationPasswordLoginHelperTest — covering the authenticated fallback, pending-vs-failed banner suppression, auto re-detect on credential establishment, and the terminal-mint-failure banner.

@dangermattic

dangermattic commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator
1 Warning
⚠️ This PR is assigned to the milestone 26.8 ❄️. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

@wpmobilebot

wpmobilebot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22926-194a069
Build Number1493
Application IDcom.jetpack.android.prealpha
Commit194a069
Installation URL5tmvu75qfd27g
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22926-194a069
Build Number1493
Application IDorg.wordpress.android.prealpha
Commit194a069
Installation URL20tnmvflg4ga0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot

wpmobilebot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

@jkmassel jkmassel force-pushed the fix/atomic-private-site-capability-detection branch 2 times, most recently from 85cf367 to b964f35 Compare June 1, 2026 21:16
@codecov

codecov Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.08333% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 37.34%. Comparing base (ddea2c4) to head (194a069).
⚠️ Report is 1 commits behind head on release/26.8.

Files with missing lines Patch % Lines
...id/ui/accounts/login/CredentialsChangedNotifier.kt 0.00% 7 Missing ⚠️
...s/android/repositories/EditorSettingsRepository.kt 92.00% 1 Missing and 1 partial ⚠️
...nnectivity/SiteConnectivityBannerViewModelSlice.kt 90.90% 0 Missing and 1 partial ⚠️
...fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt 0.00% 1 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff                @@
##           release/26.8   #22926      +/-   ##
================================================
+ Coverage         37.33%   37.34%   +0.01%     
================================================
  Files              2319     2320       +1     
  Lines            124674   124714      +40     
  Branches          16951    16959       +8     
================================================
+ Hits              46551    46580      +29     
- Misses            74361    74370       +9     
- Partials           3762     3764       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jkmassel jkmassel force-pushed the fix/atomic-private-site-capability-detection branch from b964f35 to fd57211 Compare June 1, 2026 22:58
@jkmassel jkmassel requested a review from nbradbury June 2, 2026 01:36
@jkmassel jkmassel added this to the 26.8 ❄️ milestone Jun 2, 2026
@jkmassel jkmassel self-assigned this Jun 2, 2026
@jkmassel jkmassel marked this pull request as ready for review June 2, 2026 01:47
@nbradbury

Copy link
Copy Markdown
Contributor

@jkmassel I still see this banner immediately after logging in.

banner

However, the banner disappears if I pull-to-refresh.

@nbradbury

Copy link
Copy Markdown
Contributor

@jkmassel Claude identified this as a minor issue worth resolving before merging:

silent failure of the authenticated fallback (EditorSettingsRepository.kt:186-191) The non-success branch in fetchRouteSupportViaApplicationPasswordClient returns false with no log. If the banner reappears in the wild, there will be nothing in AppLog distinguishing "discovery failed, no creds" from "discovery failed AND authenticated probe failed." A one-line AppLog.w mirroring the discovery-failure log would make this diagnosable.

@jkmassel

jkmassel commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@jkmassel Claude identified this as a minor issue worth resolving before merging:

silent failure of the authenticated fallback (EditorSettingsRepository.kt:186-191) The non-success branch in fetchRouteSupportViaApplicationPasswordClient returns false with no log. If the banner reappears in the wild, there will be nothing in AppLog distinguishing "discovery failed, no creds" from "discovery failed AND authenticated probe failed." A one-line AppLog.w mirroring the discovery-failure log would make this diagnosable.

Addressed by bf2142e

@jkmassel

jkmassel commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@jkmassel I still see this banner immediately after logging in.

banner However, the banner disappears if I pull-to-refresh.

This was a race between the application password being created and the checks happening.

It's fixed by 3a10158 and ce19bee.

You can test this by logging out of the app entirely, then logging back in and choosing that site - you shouldn't see the banner.

@nbradbury

Copy link
Copy Markdown
Contributor

@jkmassel This is working for me and I'll approve it, but you may want to address this comment from Claude:

PR description contradicts the implementation. The "Out of scope" section says:

▎ Atomic sites with no app-password credentials whose host blocks anonymous discovery still surface the banner

But isAwaitingApplicationPassword(site) = site.isWPComAtomic && !site.hasApplicationPasswordCredentials() (EditorSettingsRepository.kt:45-46) suppresses the banner in exactly that case. Either update the description, or — more importantly — confirm the intended behavior:

  • If the headless mint (app password creation) permanently fails (not just "in progress"), the user is left with a private Atomic site, no creds, and no banner ever shown. That seems like a regression of its own. Today there's no signal distinguishing "mint in progress" from "mint failed terminally" in isAwaitingApplicationPassword. Worth confirming this is acceptable, or gating on a more precise signal from ApplicationPasswordViewModelSlice.

@nbradbury nbradbury left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

jkmassel added 5 commits June 4, 2026 09:03
Editor capability detection on Atomic sites probes the direct host via
unauthenticated REST API discovery (added in #22883). Private Atomic
hosts gate anonymous requests, so discovery fails and the My Site
"Unable to connect to your site" banner appears even though the site is
reachable. Fall back to an authenticated app-password probe against the
same host when discovery fails and credentials exist.
The authenticated app-password fallback previously returned a silent
`false` on a non-success response, and the no-credentials path was also
silent. Log both (mirroring the discovery-failure log) so a recurrence
of the connectivity banner can be diagnosed from AppLog rather than
being indistinguishable from "discovery failed, no creds".
On first login an Atomic site's application password is minted
asynchronously on the My Site screen, so the connectivity capability
fetch can race ahead of it and fail purely for lack of credentials —
flashing a false "unable to connect" banner that clears on the next
refresh. Treat a credential-less fetch as pending rather than a
connection failure, mirroring the existing offline suppression; the
application-password card already owns the no-credentials state.
The connectivity banner's capability fetch and the application-password
mint both run on the My Site screen with no ordering, so on first login
the fetch loses the race and capability flags aren't detected until the
next resume/refresh. Add an app-scoped CredentialsChangedNotifier that
both credential-establishment paths (headless mint and interactive
login) emit to, and have the connectivity slice re-run detection on it —
re-reading a fresh site so it observes the just-persisted credentials.
This also closes the asymmetry where only the interactive path announced
credential changes globally.
The four-way suppression condition tripped detekt's ComplexCondition
threshold once the pending-credentials term was added. Lift it into a
named `suppressBanner` flag — clearer intent, and out of the
if-condition detekt inspects. No behavior change.
@jkmassel jkmassel force-pushed the fix/atomic-private-site-capability-detection branch from d27828b to 77f2cb1 Compare June 4, 2026 15:03
@jkmassel jkmassel enabled auto-merge (squash) June 4, 2026 15:05
@jkmassel jkmassel disabled auto-merge June 4, 2026 15:05
The pending-credentials suppression also hid the banner when the headless
mint *terminally* failed — and the application-password card can't render
for private Atomic sites either (its authorize-URL discovery hits the same
gated host), so the user was left with a broken site and no signal at all.

Track terminal mint failure in the renamed ApplicationPasswordMonitor (was
CredentialsChangedNotifier) and suppress only while provisioning is
genuinely in flight; once the mint fails, surface the banner. The monitor
also wakes capability detection on failure so the banner appears without
waiting for the next resume/refresh.
…ils"

This reverts commit 357fd82.

The terminal-failure surfacing called onMintFailed on every non-success,
including transient failures, so a transient first-login mint failure
latched hasMintFailed and showed the false connectivity banner this PR
set out to suppress. Revert to the pending-suppression behavior; the
durable single-source-of-truth redesign is tracked in #22942.
@jkmassel

jkmassel commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Atomic sites with no app-password credentials whose host blocks anonymous discovery still surface the banner

This is a fairly narrow case, so I'm going to leave it alone for this PR and work toward something a bit more comprehensive in trunk (see #22942)

@jkmassel jkmassel merged commit 840c7c0 into release/26.8 Jun 4, 2026
23 checks passed
@jkmassel jkmassel deleted the fix/atomic-private-site-capability-detection branch June 4, 2026 17:12
jkmassel added a commit that referenced this pull request Jun 4, 2026
#22926's first-login hardening signalled credential establishment through a
process-global CredentialsChangedNotifier that the banner collected against a
re-read of getSelectedSite() — the staleness race called out in #22942.
EditorCapabilityDetector.refresh(storedSite), called from the mint path on the
exact mutated SiteModel, replaces that coordination, so drop the notifier and
its wiring in ApplicationPasswordLoginHelper.
jkmassel added a commit that referenced this pull request Jun 4, 2026
* Bump version number

* Update draft release notes for 26.8

* Update draft release notes for Jetpack 26.8.

* Release Notes: add new section for next version (26.9)

* Use wordpress-rs 0.4.0

* Merge strings from libraries for translation

* Freeze strings for translation

* Keep JNA/UniFFI classes so wordpress-rs survives R8 in release builds (#22916)

AGP 9's R8 strips the @com.sun.jna.Structure.FieldOrder annotation that
wordpress-rs JNA bindings read reflectively at runtime, causing a launch
crash in minified release builds. Keep the JNA classes, UniFFI bindings,
and the annotation so native struct layouts still resolve.

* Add temporary release notes

* Merge release_notes/26.8 into release/26.8 (#22917)

* Update WordPress `PlayStoreStrings.po` for version 26.8

* Update Jetpack `PlayStoreStrings.po` for version 26.8

* Update translations

* Update translations

* Update WordPress metadata translations for 26.8

* Update Jetpack metadata translations for 26.8

* Bump version number

* Merge release_notes/26.8 into release/26.8 (#22922)

* Update editorialized release notes

* Update WordPress `PlayStoreStrings.po` for version 26.8

* Update Jetpack `PlayStoreStrings.po` for version 26.8

---------

Co-authored-by: Tony Li <tony.li@automattic.com>

* Update WordPress metadata translations for 26.8

* Update Jetpack metadata translations for 26.8

* Bump version number

* Bump version number

* Fix Czech confirm_remove_site translation breaking release lint

The 26.8 translation sync introduced a doubled backslash (\\") in the
Czech confirm_remove_site string. Android reads \\ as an escaped literal
backslash, which hides the %s placeholder from lint, so StringFormatMatches
fails: ChooseSiteActivity supplies 1 argument but the string is seen as
requiring 0.

Use a single-backslash escape (\") to match the English source and every
other locale.

* Fix Czech delete-confirmation translations breaking release lint

term_delete_confirmation_message carried the same doubled backslash (\\")
as confirm_remove_site, hiding its %1$s placeholder so lint sees 0 args
while TermsDataViewActivity supplies 1 (StringFormatMatches). It was the
second such error; lint prints only the first failure, so it surfaced
once confirm_remove_site was fixed.

Also fix the two identical siblings delete_menu_item_confirmation_message
and delete_menu_confirmation_message, which share the defect.

Verified locally: :WordPress:lintWordpressRelease BUILD SUCCESSFUL, 0 errors.

* Bump version number

* Merge release_notes/26.8 into release/26.8 (#22937)

* Update release notes

* Update WordPress `PlayStoreStrings.po` for version 26.8

* Update Jetpack `PlayStoreStrings.po` for version 26.8

---------

Co-authored-by: Tony Li <tony.li@automattic.com>

* Fix false connectivity banner on private Atomic sites (#22926)

* Fix false connectivity banner on private Atomic sites

Editor capability detection on Atomic sites probes the direct host via
unauthenticated REST API discovery (added in #22883). Private Atomic
hosts gate anonymous requests, so discovery fails and the My Site
"Unable to connect to your site" banner appears even though the site is
reachable. Fall back to an authenticated app-password probe against the
same host when discovery fails and credentials exist.

* Log authenticated direct-host probe failures for diagnosability

The authenticated app-password fallback previously returned a silent
`false` on a non-success response, and the no-credentials path was also
silent. Log both (mirroring the discovery-failure log) so a recurrence
of the connectivity banner can be diagnosed from AppLog rather than
being indistinguishable from "discovery failed, no creds".

* Suppress the connectivity banner while the app password is being minted

On first login an Atomic site's application password is minted
asynchronously on the My Site screen, so the connectivity capability
fetch can race ahead of it and fail purely for lack of credentials —
flashing a false "unable to connect" banner that clears on the next
refresh. Treat a credential-less fetch as pending rather than a
connection failure, mirroring the existing offline suppression; the
application-password card already owns the no-credentials state.

* Re-detect editor capabilities when an app password is established

The connectivity banner's capability fetch and the application-password
mint both run on the My Site screen with no ordering, so on first login
the fetch loses the race and capability flags aren't detected until the
next resume/refresh. Add an app-scoped CredentialsChangedNotifier that
both credential-establishment paths (headless mint and interactive
login) emit to, and have the connectivity slice re-run detection on it —
re-reading a fresh site so it observes the just-persisted credentials.
This also closes the asymmetry where only the interactive path announced
credential changes globally.

* Hoist connectivity banner suppression into a named flag

The four-way suppression condition tripped detekt's ComplexCondition
threshold once the pending-credentials term was added. Lift it into a
named `suppressBanner` flag — clearer intent, and out of the
if-condition detekt inspects. No behavior change.

* Surface the connectivity banner when the app-password mint fails

The pending-credentials suppression also hid the banner when the headless
mint *terminally* failed — and the application-password card can't render
for private Atomic sites either (its authorize-URL discovery hits the same
gated host), so the user was left with a broken site and no signal at all.

Track terminal mint failure in the renamed ApplicationPasswordMonitor (was
CredentialsChangedNotifier) and suppress only while provisioning is
genuinely in flight; once the mint fails, surface the banner. The monitor
also wakes capability detection on failure so the banner appears without
waiting for the next resume/refresh.

* Revert "Surface the connectivity banner when the app-password mint fails"

This reverts commit 357fd82.

The terminal-failure surfacing called onMintFailed on every non-success,
including transient failures, so a transient first-login mint failure
latched hasMintFailed and showed the false connectivity banner this PR
set out to suppress. Revert to the pending-suppression behavior; the
durable single-source-of-truth redesign is tracked in #22942.

---------

Co-authored-by: Automattic Release Bot <mobile+wpmobilebot@automattic.com>
Co-authored-by: Tony Li <tony.li@automattic.com>
jkmassel added a commit that referenced this pull request Jun 4, 2026
#22926's first-login hardening signalled credential establishment through a
process-global CredentialsChangedNotifier that the banner collected against a
re-read of getSelectedSite() — the staleness race called out in #22942.
EditorCapabilityDetector.refresh(storedSite), called from the mint path on the
exact mutated SiteModel, replaces that coordination, so drop the notifier and
its wiring in ApplicationPasswordLoginHelper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants