Repository: wordpress-mobile/WordPress-iOS
Pull request #24277: Ensure current site is always displayed in Sidebar List
Author: login: pmusolino
Labels: name: iPad
name: UI
description: User interface bugs
Created: 2025-03-20T17:12:09Z | Merged: 2025-03-21T16:25:27Z

Fixes #23569 

This PR modifies the sidebar site display logic to ensure the currently selected site is always visible, regardless of its position in the recent sites list or alphabetical order.

## Changes Made
- Modified BlogListViewModel.topSites to prioritize displaying the currently selected site
- Implemented three-tier priority system for site display:
1. Current site (always shown)
2. Recent sites (up to display limit)
3. Other sites (alphabetically sorted)
- Added some unit tests

## Testing Instructions
On iPad/simulator
1. Change `SidebarView.displayedSiteLimit` to a number smaller than `RecentSitesService.maxSiteCount`
2. Log into an account with more sites than the display limit
3. Select a site that would normally be pushed out of view (e.g., alphabetically last, you can create a site named "Zoo")
5. Verify that the selected site remains visible in the sidebar

## Screenshot
![Simulator Screenshot - iPad Pro 13-inch (M4) - 2025-03-20 at 17 39 04](https://github.com/user-attachments/assets/99812ec0-0c4d-4975-8d76-3108fc9c50eb)


## Regression Notes
1. Potential unintended areas of impact


2. What I did to test those areas of impact (or what existing automated tests I relied on)


3. What automated tests I added (or what prevented me from doing so)

PR submission checklist:

- [ ] I have completed the Regression Notes.
- [ ] I have considered adding unit tests for my changes.
- [ ] I have considered adding accessibility improvements for my changes.
- [ ] I have considered if this change warrants user-facing release notes and have added them to `RELEASE-NOTES.txt` if necessary.

Testing checklist:
- [ ] WordPress.com sites and self-hosted Jetpack sites.
- [ ] Portrait and landscape orientations.
- [ ] Light and dark modes.
- [ ] Fonts: Larger, smaller and bold text.
- [ ] High contrast.
- [ ] VoiceOver.
- [ ] Languages with large words or with letters/accents not frequently used in English.
- [ ] Right-to-left languages. (Even if translation isn’t complete, formatting should still respect the right-to-left layout)
- [ ] iPhone and iPad. 
- [ ] Multi-tasking: Split view and Slide over. (iPad)


Linked issue #23569: The current site may not be displayed in the sidebar
### Expected behavior

We display a maximal number of `SidebarView.displayedSiteLimit` sites on the sidebar. The sites on the sidebar contains (in priority order) the current site, other recently used sites, and the rest of the user's sites.

### Actual behavior

The issue with current implementation is `SidebarView.displayedSiteLimit` is hard-coded as exactly the same as `RecentSitesService.maxSiteCount`. 

The current site may not be displayed if we change `SidebarView.displayedSiteLimit` to be a smaller number.

### Steps to reproduce the behavior

1. Change `SidebarView.displayedSiteLimit` to be a number smaller than `RecentSitesService.maxSiteCount`.
2. Make sure there are at least `RecentSitesService.maxSiteCount` sites in a WP.com account.
3. Create a site named "Zoo" (any name that comes last in alphabetical order) in the WP.com account.
4. Launch the Jetpack app and switch to a few different sites.
5. Select the "Zoo" site.
6. Open the sidebar, and check if "Zoo" is displayed in the sidebar.

##### Tested on [device], iOS [version], Jetpack iOS / WordPress iOS [version]

Linked issue comments: user: login: dangermattic
created_at: 2024-09-09T00:33:52Z
body: Thanks for reporting! 👍 <!-- generated_by_dangermattic -->

author_association: COLLABORATOR
user: login: kean
created_at: 2024-09-09T13:41:28Z
body: That's a good point. I'd suggest changing `RecentSitesService` so that it doesn't artificially limited the number of most recent sites. It should be UI's job to determine how many to display.
author_association: CONTRIBUTOR
user: login: kean
created_at: 2024-09-12T17:04:52Z
body: I'm changing the tag to "Tech Debt" since there is no defect at the moment.
author_association: CONTRIBUTOR

Diff:
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 69b86e82f822..0bb15df6d0b3 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -1,6 +1,6 @@
 25.9
 -----
-
+* [*] Updated site display logic in sidebar to prioritize showing the currently selected site on iPad. [#24277]
 
 25.8
 -----
diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift
index 8ed9d257aa22..816166f9c737 100644
--- a/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift	
+++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift	
@@ -23,6 +23,25 @@ final class BlogListViewModel: NSObject, ObservableObject {
 
     var onAddSiteTapped: (AddSiteMenuViewModel.Selection) -> Void = { _ in }
 
+    weak var sidebarViewModel: SidebarViewModel?
+
+    var currentSite: BlogListSiteViewModel? {
+        guard let sidebarViewModel,
+              case let .blog(selectedBlogID) = sidebarViewModel.selection else {
+            return nil
+        }
+
+        do {
+            if let blog = try contextManager.mainContext.existingObject(with: selectedBlogID.objectID) as? Blog {
+                return BlogListSiteViewModel(blog: blog)
+            }
+        } catch {
+            DDLogError("Failed to fetch blog with ID \(selectedBlogID): \(error)")
+        }
+
+        return nil
+    }
+
     init(configuration: BlogListConfiguration = .defaultConfig,
          contextManager: ContextManager = ContextManager.shared,
          recentSitesService: RecentSitesService = RecentSitesService(),
diff --git a/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift b/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
index b2fe93f3c2ad..5dbd11a3cbe6 100644
--- a/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
+++ b/WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
@@ -10,6 +10,7 @@ final class SidebarViewController: UIHostingController<AnyView> {
 
     init(viewModel: SidebarViewModel) {
         self.viewModel = viewModel
+        self.viewModel.blogListViewModel.sidebarViewModel = viewModel
         super.init(rootView: AnyView(SidebarView(viewModel: viewModel, blogListViewModel: viewModel.blogListViewModel)))
         self.title = Strings.sectionMySites
     }
@@ -25,7 +26,7 @@ final class SidebarViewController: UIHostingController<AnyView> {
     }
 }
 
-private struct SidebarView: View {
+struct SidebarView: View {
     @ObservedObject var viewModel: SidebarViewModel
     @ObservedObject var blogListViewModel: BlogListViewModel
     @StateObject private var notificationsButtonViewModel = NotificationsButtonViewModel()
@@ -221,21 +222,35 @@ private struct SidebarProfileContainerView: View {
     }
 }
 
-private extension BlogListViewModel {
-    /// Returns the most recent sites and mixes and mixes in the rest of the sites
-    /// until the display limimt is reached.
+extension BlogListViewModel {
+    /// Returns a list of sites to display in the sidebar, ensuring that:
+    /// 1. The current site is always included
+    /// 2. The most recent sites are included up to the display limit
+    /// 3. The sites are sorted alphabetically
     var topSites: [BlogListSiteViewModel] {
-        var topSites = recentSites.prefix(SidebarView.displayedSiteLimit)
-        var encounteredIDs = Set(topSites.map(\.id))
-        for site in allSites where !encounteredIDs.contains(site.id) {
-            if topSites.count >= SidebarView.displayedSiteLimit {
+        var displaySites = [BlogListSiteViewModel]()
+        var encounteredIDs = Set<TaggedManagedObjectID<Blog>>()
+
+        // Ensure the current site is included (if there is one)
+        if let currentSite {
+            displaySites.append(currentSite)
+            encounteredIDs.insert(currentSite.id)
+        }
+
+        // Add recent sites up to the limit, if we still have space, add other sites
+        for site in recentSites + allSites {
+            if displaySites.count >= SidebarView.displayedSiteLimit {
                 break
             }
-            encounteredIDs.insert(site.id)
-            topSites.append(site)
-        }
-        return Array(topSites).sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending
+
+            if !encounteredIDs.contains(site.id) {
+                displaySites.append(site)
+                encounteredIDs.insert(site.id)
+            }
         }
+
+        // Sort the sites alphabetically
+        return displaySites.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending }
     }
 }
 
diff --git a/WordPress/WordPressTest/BlogListViewModelTests.swift b/WordPress/WordPressTest/BlogListViewModelTests.swift
index 985659d7c6d2..cdcd3842e080 100644
--- a/WordPress/WordPressTest/BlogListViewModelTests.swift
+++ b/WordPress/WordPressTest/BlogListViewModelTests.swift
@@ -70,4 +70,120 @@ final class BlogListViewModelTests: CoreDataTestCase {
         let displayedNames = viewModel.allSites.map(\.title)
         XCTAssertEqual(displayedNames, [".Org", "51 Zone", "A", "C"])
     }
+
+    func testTopSitesIncludesCurrentSite() throws {
+        // Given: Create test sites
+        let siteA = BlogBuilder(mainContext)
+            .with(siteName: "A Site")
+            .with(dotComID: 1)
+            .build()
+
+        let siteB = BlogBuilder(mainContext)
+            .with(siteName: "B Site")
+            .with(dotComID: 2)
+            .build()
+
+        let siteC = BlogBuilder(mainContext)
+            .with(siteName: "C Site")
+            .with(dotComID: 3)
+            .build()
+
+        let siteZ = BlogBuilder(mainContext)
+            .with(siteName: "Z Site")
+            .with(dotComID: 4)
+            .build()
+
+        try mainContext.save()
+
+        // When:  Set up recent sites (excluding Z)
+        recentSitesService.touch(blog: siteA)
+        recentSitesService.touch(blog: siteB)
+        recentSitesService.touch(blog: siteC)
+
+        setupViewModel()
+
+        // Mock the sidebarViewModel and set the current site to Z (which is not in recent sites)
+        let mockSidebarViewModel = SidebarViewModel()
+        mockSidebarViewModel.selection = .blog(TaggedManagedObjectID(siteZ))
+        viewModel.sidebarViewModel = mockSidebarViewModel
+
+        // Then: Verify Z is included in topSites even though it's not a recent site
+        XCTAssertTrue(viewModel.topSites.contains(where: { $0.id == TaggedManagedObjectID(siteZ) }))
+    }
+
+    func testTopSitesRespectsSiteLimit() throws {
+        // Given: Create 6 test sites (more than the current display limit of 4)
+        let sites = (1...6).map { i in
+            return BlogBuilder(mainContext)
+                .with(siteName: "Site \(i)")
+                .with(dotComID: i)
+                .build()
+        }
+
+        try mainContext.save()
+
+        // When: Touch all sites to make them recent
+        sites.forEach { recentSitesService.touch(blog: $0) }
+
+        setupViewModel()
+
+        // Then: Verify topSites doesn't exceed the display limit
+        XCTAssertEqual(viewModel.topSites.count, SidebarView.displayedSiteLimit)
+    }
+
+    func testTopSitesPrioritizesCurrentSiteThenRecentSites() throws {
+        // Given
+        // Create test sites with names that would be sorted differently alphabetically
+        let siteA = BlogBuilder(mainContext)
+            .with(siteName: "A Site")
+            .with(dotComID: 1)
+            .build()
+
+        let siteB = BlogBuilder(mainContext)
+            .with(siteName: "B Site")
+            .with(dotComID: 2)
+            .build()
+
+        let siteY = BlogBuilder(mainContext)
+            .with(siteName: "Y Site")
+            .with(dotComID: 3)
+            .build()
+
+        let siteZ = BlogBuilder(mainContext)
+            .with(siteName: "Z Site")
+            .with(dotComID: 4)
+            .build()
+
+        try mainContext.save()
+
+        // When
+        // Set up recent sites in order: B, Y
+        recentSitesService.touch(blog: siteB)
+        recentSitesService.touch(blog: siteY)
+
+        setupViewModel()
+
+        // Make Z the current site
+        let mockSidebarViewModel = SidebarViewModel()
+        mockSidebarViewModel.selection = .blog(TaggedManagedObjectID(siteZ))
+        viewModel.sidebarViewModel = mockSidebarViewModel
+
+        // Then
+        // Verify all sites are sorted alphabetically, but Z is included despite not being recent
+        let siteNames = viewModel.topSites.map(\.title)
+        XCTAssertTrue(siteNames.contains("Z Site"), "Current site should be included")
+        XCTAssertTrue(isSorted(siteNames), "Sites should be sorted alphabetically")
+    }
+}
+
+private extension BlogListViewModelTests {
+    // Helper method to check if an array is sorted
+    func isSorted(_ array: [String]) -> Bool {
+        for i in 0..<(array.count - 1) {
+            if array[i].localizedCaseInsensitiveCompare(array[i + 1]) == .orderedDescending {
+                return false
+            }
+        }
+        return true
+    }
 }


Reviews:
user: login: kean
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:21:14Z
user: login: kean
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T17:38:16Z
user: login: pmusolino
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:15:09Z
user: login: pmusolino
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:29:23Z
user: login: pmusolino
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:48:40Z
user: login: pmusolino
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:59:25Z

Review comments:
diff_hunk: @@ -221,21 +222,47 @@ private struct SidebarProfileContainerView: View {
     }
 }
 
-private extension BlogListViewModel {
-    /// Returns the most recent sites and mixes and mixes in the rest of the sites
-    /// until the display limimt is reached.
+extension BlogListViewModel {
+    /// Returns a list of sites to display in the sidebar, ensuring that:
+    /// 1. The current site is always included
+    /// 2. The most recent sites are included up to the display limit
+    /// 3. The sites are sorted alphabetically
     var topSites: [BlogListSiteViewModel] {
-        var topSites = recentSites.prefix(SidebarView.displayedSiteLimit)
-        var encounteredIDs = Set(topSites.map(\.id))
-        for site in allSites where !encounteredIDs.contains(site.id) {
-            if topSites.count >= SidebarView.displayedSiteLimit {
+        var displaySites = [BlogListSiteViewModel]()
path: WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
user: login: kean
body: Have you tried the steps from https://github.com/wordpress-mobile/WordPress-iOS/issues/23569? I vaguely remember this code, but I think we opened it just in case. I doubt it's a real issue. `recentSites` always has your most recently selected site as the first item of the array. It's impossible for it to not get displayed. I might be wrong – not entirely sure about this code.
created_at: 2025-03-20T17:21:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,21 +222,47 @@ private struct SidebarProfileContainerView: View {
     }
 }
 
-private extension BlogListViewModel {
-    /// Returns the most recent sites and mixes and mixes in the rest of the sites
-    /// until the display limimt is reached.
+extension BlogListViewModel {
+    /// Returns a list of sites to display in the sidebar, ensuring that:
+    /// 1. The current site is always included
+    /// 2. The most recent sites are included up to the display limit
+    /// 3. The sites are sorted alphabetically
     var topSites: [BlogListSiteViewModel] {
-        var topSites = recentSites.prefix(SidebarView.displayedSiteLimit)
-        var encounteredIDs = Set(topSites.map(\.id))
-        for site in allSites where !encounteredIDs.contains(site.id) {
-            if topSites.count >= SidebarView.displayedSiteLimit {
+        var displaySites = [BlogListSiteViewModel]()
+        var encounteredIDs = Set<TaggedManagedObjectID<Blog>>()
+
+        // Ensure the current site is included (if there is one)
+        if let currentSite {
+            displaySites.append(currentSite)
+            encounteredIDs.insert(currentSite.id)
+        }
+
+        // Add recent sites up to the limit
+        for site in recentSites {
path: WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
user: login: kean
body: (nit) you should be able to merge these two loops in one:  `for site in recentSites + allSites`
created_at: 2025-03-20T17:34:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +23,17 @@ final class BlogListViewModel: NSObject, ObservableObject {
 
     var onAddSiteTapped: (AddSiteMenuViewModel.Selection) -> Void = { _ in }
 
+    weak var sidebarViewModel: SidebarViewModel?
+
+    var currentSite: BlogListSiteViewModel? {
+        guard let sidebarViewModel,
+              case let .blog(selectedBlogID) = sidebarViewModel.selection else {
+            return nil
+        }
+
+        return allSites.first { $0.id == selectedBlogID }
path: WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift
user: login: kean
body: (nit) I'd probably suggest using `try mainContext.existingObject(with: objectID)` instead of iterating over `allSites`.
created_at: 2025-03-20T17:37:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,4 +70,120 @@ final class BlogListViewModelTests: CoreDataTestCase {
         let displayedNames = viewModel.allSites.map(\.title)
         XCTAssertEqual(displayedNames, [".Org", "51 Zone", "A", "C"])
     }
+
+    func test_topSites_includes_current_site() throws {
path: WordPress/WordPressTest/BlogListViewModelTests.swift
user: login: kean
body: (nit) please use camel case. Btw, for new tests (new suites), feel free to use Swift Testing.
created_at: 2025-03-20T17:38:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,21 +222,47 @@ private struct SidebarProfileContainerView: View {
     }
 }
 
-private extension BlogListViewModel {
-    /// Returns the most recent sites and mixes and mixes in the rest of the sites
-    /// until the display limimt is reached.
+extension BlogListViewModel {
+    /// Returns a list of sites to display in the sidebar, ensuring that:
+    /// 1. The current site is always included
+    /// 2. The most recent sites are included up to the display limit
+    /// 3. The sites are sorted alphabetically
     var topSites: [BlogListSiteViewModel] {
-        var topSites = recentSites.prefix(SidebarView.displayedSiteLimit)
-        var encounteredIDs = Set(topSites.map(\.id))
-        for site in allSites where !encounteredIDs.contains(site.id) {
-            if topSites.count >= SidebarView.displayedSiteLimit {
+        var displaySites = [BlogListSiteViewModel]()
path: WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
user: login: pmusolino
body: Yes, I followed the steps from the original issue, and I confirm I was able to replicate the issue, especially if you have multiple sites and you create or select a new one that starts with `W` or `Z`.

Screenshot:
![Simulator Screenshot - iPad Pro 13-inch (M4) - 2025-03-21 at 14 13 18](https://github.com/user-attachments/assets/951d7b14-2a2c-4a1f-ba99-e695af04779c)


created_at: 2025-03-21T13:15:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -221,21 +222,47 @@ private struct SidebarProfileContainerView: View {
     }
 }
 
-private extension BlogListViewModel {
-    /// Returns the most recent sites and mixes and mixes in the rest of the sites
-    /// until the display limimt is reached.
+extension BlogListViewModel {
+    /// Returns a list of sites to display in the sidebar, ensuring that:
+    /// 1. The current site is always included
+    /// 2. The most recent sites are included up to the display limit
+    /// 3. The sites are sorted alphabetically
     var topSites: [BlogListSiteViewModel] {
-        var topSites = recentSites.prefix(SidebarView.displayedSiteLimit)
-        var encounteredIDs = Set(topSites.map(\.id))
-        for site in allSites where !encounteredIDs.contains(site.id) {
-            if topSites.count >= SidebarView.displayedSiteLimit {
+        var displaySites = [BlogListSiteViewModel]()
+        var encounteredIDs = Set<TaggedManagedObjectID<Blog>>()
+
+        // Ensure the current site is included (if there is one)
+        if let currentSite {
+            displaySites.append(currentSite)
+            encounteredIDs.insert(currentSite.id)
+        }
+
+        // Add recent sites up to the limit
+        for site in recentSites {
path: WordPress/Classes/ViewRelated/System/Sidebar/SidebarViewController.swift
user: login: pmusolino
body: Good suggestion f802776192ae2130d511772c4414232b1cca5aea
created_at: 2025-03-21T13:29:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +23,17 @@ final class BlogListViewModel: NSObject, ObservableObject {
 
     var onAddSiteTapped: (AddSiteMenuViewModel.Selection) -> Void = { _ in }
 
+    weak var sidebarViewModel: SidebarViewModel?
+
+    var currentSite: BlogListSiteViewModel? {
+        guard let sidebarViewModel,
+              case let .blog(selectedBlogID) = sidebarViewModel.selection else {
+            return nil
+        }
+
+        return allSites.first { $0.id == selectedBlogID }
path: WordPress/Classes/ViewRelated/Blog/Site Picker/BlogList/BlogListViewModel.swift
user: login: pmusolino
body: 👍 implemented here 5bcfca312002e6c794963900ce22e260631f4225
created_at: 2025-03-21T13:48:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,4 +70,120 @@ final class BlogListViewModelTests: CoreDataTestCase {
         let displayedNames = viewModel.allSites.map(\.title)
         XCTAssertEqual(displayedNames, [".Org", "51 Zone", "A", "C"])
     }
+
+    func test_topSites_includes_current_site() throws {
path: WordPress/WordPressTest/BlogListViewModelTests.swift
user: login: pmusolino
body: Updated here 8aeec842f646a7e832e43cda0cbc0e28aba46d41 👍 
created_at: 2025-03-21T13:59:25Z
author_association: CONTRIBUTOR

PR comments:
user: login: dangermattic
created_at: 2025-03-20T17:12:51Z
body: <!--
  0 Errors
  1 Warning: This PR is assigned to the mil...
  0 Messages
  0 Markdowns
-->
<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true" data-kind="Warning">
          1 Warning
      </th>
     </tr>
  </thead>
  <tbody>
    <tr>
      <td>:warning:</td>
      <td data-sticky="false">This PR is assigned to the milestone <a href="https://github.com/wordpress-mobile/WordPress-iOS/milestone/308">25.9</a>. The due date for this milestone has already passed.<br />
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.</td>
    </tr>
  </tbody>
</table>

<p align="right" data-meta="generated_by_pr-check">
  Generated by :no_entry_sign: <a href="https://danger.systems/">Danger</a>
</p>

author_association: COLLABORATOR
user: login: wpmobilebot
created_at: 2025-03-20T17:26:43Z
body: <!-- REUSE_ID: prototype-build-link-WPiOS-One-Offs -->

<details><summary><img alt='WordPress Alpha' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/wordpress.png' width='20px' />📲 You can test the changes from this Pull Request in <b>WordPress Alpha</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='7' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Finstall.appcenter.ms%2Forgs%2Fautomattic%2Fapps%2FWPiOS-One-Offs%2Freleases%2F11793' width='250' height='250' /></td>
  <td><b>App Name</b></td><td><img alt='WordPress Alpha' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/wordpress.png' width='20px' /> WordPress Alpha</td>
</tr>
<tr><td><b>Configuration</b></td><td>Release-Alpha</td></tr>
<tr><td><b>Build Number</b></td><td>pr24277-8aeec84</td></tr>
<tr><td><b>Version</b></td><td>25.8</td></tr>
<tr><td><b>Bundle ID</b></td><td>org.wordpress.alpha</td></tr>
<tr><td><b>Commit</b></td><td>8aeec842f646a7e832e43cda0cbc0e28aba46d41</td></tr>
<tr><td><b>App Center Build</b></td><td><a href='https://install.appcenter.ms/orgs/automattic/apps/WPiOS-One-Offs/releases/11793'>WPiOS - One-Offs #11793</a></td></tr>
</table>
<em>Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.</em>
</details>

author_association: CONTRIBUTOR
user: login: wpmobilebot
created_at: 2025-03-20T17:27:04Z
body: <!-- REUSE_ID: prototype-build-link-jetpack-installable-builds -->

<details><summary><img alt='Jetpack Alpha' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/jetpack.png' width='20px' />📲 You can test the changes from this Pull Request in <b>Jetpack Alpha</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='7' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Finstall.appcenter.ms%2Forgs%2Fautomattic%2Fapps%2Fjetpack-installable-builds%2Freleases%2F10818' width='250' height='250' /></td>
  <td><b>App Name</b></td><td><img alt='Jetpack Alpha' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/jetpack.png' width='20px' /> Jetpack Alpha</td>
</tr>
<tr><td><b>Configuration</b></td><td>Release-Alpha</td></tr>
<tr><td><b>Build Number</b></td><td>pr24277-8aeec84</td></tr>
<tr><td><b>Version</b></td><td>25.8</td></tr>
<tr><td><b>Bundle ID</b></td><td>com.jetpack.alpha</td></tr>
<tr><td><b>Commit</b></td><td>8aeec842f646a7e832e43cda0cbc0e28aba46d41</td></tr>
<tr><td><b>App Center Build</b></td><td><a href='https://install.appcenter.ms/orgs/automattic/apps/jetpack-installable-builds/releases/10818'>jetpack-installable-builds #10818</a></td></tr>
</table>
<em>Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.</em>
</details>

author_association: CONTRIBUTOR