Skip to content

Commit 506ca74

Browse files
authored
Merge pull request #5241 from woocommerce/issue/2520-site-list-refresh
Keep sites in site picker up to date
2 parents a436bda + 627cee9 commit 506ca74

File tree

10 files changed

+145
-14
lines changed

10 files changed

+145
-14
lines changed

Networking/Networking/Model/Copiable/Models+Copiable.generated.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1138,6 +1138,42 @@ extension ShippingLabelPurchase {
11381138
}
11391139
}
11401140

1141+
extension Site {
1142+
public func copy(
1143+
siteID: CopiableProp<Int64> = .copy,
1144+
name: CopiableProp<String> = .copy,
1145+
description: CopiableProp<String> = .copy,
1146+
url: CopiableProp<String> = .copy,
1147+
plan: CopiableProp<String> = .copy,
1148+
isWooCommerceActive: CopiableProp<Bool> = .copy,
1149+
isWordPressStore: CopiableProp<Bool> = .copy,
1150+
timezone: CopiableProp<String> = .copy,
1151+
gmtOffset: CopiableProp<Double> = .copy
1152+
) -> Site {
1153+
let siteID = siteID ?? self.siteID
1154+
let name = name ?? self.name
1155+
let description = description ?? self.description
1156+
let url = url ?? self.url
1157+
let plan = plan ?? self.plan
1158+
let isWooCommerceActive = isWooCommerceActive ?? self.isWooCommerceActive
1159+
let isWordPressStore = isWordPressStore ?? self.isWordPressStore
1160+
let timezone = timezone ?? self.timezone
1161+
let gmtOffset = gmtOffset ?? self.gmtOffset
1162+
1163+
return Site(
1164+
siteID: siteID,
1165+
name: name,
1166+
description: description,
1167+
url: url,
1168+
plan: plan,
1169+
isWooCommerceActive: isWooCommerceActive,
1170+
isWordPressStore: isWordPressStore,
1171+
timezone: timezone,
1172+
gmtOffset: gmtOffset
1173+
)
1174+
}
1175+
}
1176+
11411177
extension SitePlugin {
11421178
public func copy(
11431179
siteID: CopiableProp<Int64> = .copy,

Networking/Networking/Model/Site.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Codegen
33

44
/// Represents a WordPress.com Site.
55
///
6-
public struct Site: Decodable, Equatable, GeneratedFakeable {
6+
public struct Site: Decodable, Equatable, GeneratedFakeable, GeneratedCopiable {
77

88
/// WordPress.com Site Identifier.
99
///

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
7.9
44
-----
5+
- [*] Fix: after disconnecting a site or connecting to a new site, the sites in site picker (Settings > Switch Store) should be updated accordingly. The only exception is when the newly disconnected site is the currently selected site. [https://github.com/woocommerce/woocommerce-ios/pull/5241]
56

67

78
7.8

Storage/Storage/Tools/StorageType+Extensions.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public extension StorageType {
2626
return firstObject(ofType: Site.self, matching: predicate)
2727
}
2828

29+
/// Retrieves all stored sites.
30+
///
31+
func loadAllSites() -> [Site] {
32+
allObjects(ofType: Site.self, matching: nil, sortedBy: nil)
33+
}
34+
2935
// MARK: - Orders
3036

3137
/// Retrieves the Stored Order.

WooCommerce/Classes/Authentication/Epilogue/StorePickerViewController.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ enum StorePickerConfiguration {
4747

4848
/// Allows the user to pick which WordPress.com (OR) Jetpack-Connected-Store we should set up as the Main Store.
4949
///
50-
class StorePickerViewController: UIViewController {
50+
final class StorePickerViewController: UIViewController {
5151

5252
/// StorePickerViewController Delegate
5353
///
@@ -243,8 +243,16 @@ private extension StorePickerViewController {
243243
}
244244

245245
func refreshResults() {
246-
try? resultsController.performFetch()
246+
refetchSitesAndUpdateState()
247247
ServiceLocator.analytics.track(.sitePickerStoresShown, withProperties: ["num_of_stores": resultsController.numberOfObjects])
248+
249+
synchronizeSites { [weak self] _ in
250+
self?.refetchSitesAndUpdateState()
251+
}
252+
}
253+
254+
func refetchSitesAndUpdateState() {
255+
try? resultsController.performFetch()
248256
state = StorePickerState(sites: resultsController.fetchedObjects)
249257
}
250258

@@ -262,6 +270,15 @@ private extension StorePickerViewController {
262270
}
263271
}
264272

273+
// MARK: - Syncing
274+
//
275+
private extension StorePickerViewController {
276+
func synchronizeSites(onCompletion: @escaping (Result<Void, Error>) -> Void) {
277+
let action = AccountAction.synchronizeSites(selectedSiteID: currentlySelectedSite?.siteID, onCompletion: onCompletion)
278+
ServiceLocator.stores.dispatch(action)
279+
}
280+
}
281+
265282

266283
// MARK: - Notifications
267284
//
@@ -587,7 +604,7 @@ extension StorePickerViewController: UITableViewDataSource {
587604
cell.name = site.name
588605
cell.url = site.url
589606
cell.allowsCheckmark = state.multipleStoresAvailable
590-
cell.displaysCheckmark = currentlySelectedSite == site
607+
cell.displaysCheckmark = currentlySelectedSite?.siteID == site.siteID
591608

592609
return cell
593610
}

WooCommerce/Classes/Yosemite/DefaultStoresManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ private extension DefaultStoresManager {
273273
/// Synchronizes the WordPress.com Sites, associated with the current credentials.
274274
///
275275
func synchronizeSites(onCompletion: @escaping (Result<Void, Error>) -> Void) {
276-
let action = AccountAction.synchronizeSites(onCompletion: onCompletion)
276+
let action = AccountAction.synchronizeSites(selectedSiteID: sessionManager.defaultStoreID, onCompletion: onCompletion)
277277
dispatch(action)
278278
}
279279

Yosemite/Yosemite/Actions/AccountAction.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public enum AccountAction: Action {
1010
case loadSite(siteID: Int64, onCompletion: (Site?) -> Void)
1111
case synchronizeAccount(onCompletion: (Result<Account, Error>) -> Void)
1212
case synchronizeAccountSettings(userID: Int64, onCompletion: (Result<AccountSettings, Error>) -> Void)
13-
case synchronizeSites(onCompletion: (Result<Void, Error>) -> Void)
13+
case synchronizeSites(selectedSiteID: Int64?, onCompletion: (Result<Void, Error>) -> Void)
1414
case synchronizeSitePlan(siteID: Int64, onCompletion: (Result<Void, Error>) -> Void)
1515
case updateAccountSettings(userID: Int64, tracksOptOut: Bool, onCompletion: (Result<Void, Error>) -> Void)
1616
}

Yosemite/Yosemite/Stores/AccountStore.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ public class AccountStore: Store {
4747
synchronizeAccount(onCompletion: onCompletion)
4848
case .synchronizeAccountSettings(let userID, let onCompletion):
4949
synchronizeAccountSettings(userID: userID, onCompletion: onCompletion)
50-
case .synchronizeSites(let onCompletion):
51-
synchronizeSites(onCompletion: onCompletion)
50+
case .synchronizeSites(let selectedSiteID, let onCompletion):
51+
synchronizeSites(selectedSiteID: selectedSiteID, onCompletion: onCompletion)
5252
case .synchronizeSitePlan(let siteID, let onCompletion):
5353
synchronizeSitePlan(siteID: siteID, onCompletion: onCompletion)
5454
case .updateAccountSettings(let userID, let tracksOptOut, let onCompletion):
@@ -90,11 +90,11 @@ private extension AccountStore {
9090

9191
/// Synchronizes the WordPress.com sites associated with the Network's Auth Token.
9292
///
93-
func synchronizeSites(onCompletion: @escaping (Result<Void, Error>) -> Void) {
93+
func synchronizeSites(selectedSiteID: Int64?, onCompletion: @escaping (Result<Void, Error>) -> Void) {
9494
remote.loadSites { [weak self] result in
9595
switch result {
9696
case .success(let sites):
97-
self?.upsertStoredSitesInBackground(readOnlySites: sites) {
97+
self?.upsertStoredSitesInBackground(readOnlySites: sites, selectedSiteID: selectedSiteID) {
9898
onCompletion(.success(()))
9999
}
100100
case .failure(let error):
@@ -192,9 +192,17 @@ extension AccountStore {
192192

193193
/// Updates (OR Inserts) the specified ReadOnly Site Entities into the Storage Layer.
194194
///
195-
func upsertStoredSitesInBackground(readOnlySites: [Networking.Site], onCompletion: @escaping () -> Void) {
195+
func upsertStoredSitesInBackground(readOnlySites: [Networking.Site], selectedSiteID: Int64? = nil, onCompletion: @escaping () -> Void) {
196196
let derivedStorage = sharedDerivedStorage
197197
derivedStorage.perform {
198+
// Deletes sites in storage that are not in `readOnlySites` and not the selected site.
199+
let storageSites = derivedStorage.loadAllSites()
200+
let readOnlySiteIDs = readOnlySites.map(\.siteID)
201+
storageSites.filter { readOnlySiteIDs.contains($0.siteID) == false && $0.siteID != selectedSiteID }
202+
.forEach { remotelyDeletedSite in
203+
derivedStorage.deleteObject(remotelyDeletedSite)
204+
}
205+
198206
for readOnlySite in readOnlySites {
199207
let storageSite = derivedStorage.loadSite(siteID: readOnlySite.siteID) ?? derivedStorage.insertNewObject(ofType: Storage.Site.self)
200208
storageSite.update(with: readOnlySite)

Yosemite/YosemiteTests/Mocks/MockStorageManager+Sample.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ extension MockStorageManager {
131131
return newOrder
132132
}
133133

134+
/// Inserts a new (Sample) site into the specified context.
135+
///
136+
@discardableResult
137+
func insertSampleSite(readOnlySite: Site) -> StorageSite {
138+
let newSite = viewStorage.insertNewObject(ofType: StorageSite.self)
139+
newSite.update(with: readOnlySite)
140+
141+
return newSite
142+
}
143+
134144
/// Inserts a new (Sample) PLugin into the specified context.
135145
///
136146
@discardableResult

Yosemite/YosemiteTests/Stores/AccountStoreTests.swift

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Fakes
12
import XCTest
23
@testable import Yosemite
34
@testable import Networking
@@ -7,7 +8,7 @@ import XCTest
78

89
/// AccountStore Unit Tests
910
///
10-
class AccountStoreTests: XCTestCase {
11+
final class AccountStoreTests: XCTestCase {
1112

1213
/// Mock Dispatcher!
1314
///
@@ -216,7 +217,7 @@ class AccountStoreTests: XCTestCase {
216217

217218
// When
218219
let result: Result<Void, Error> = waitFor { promise in
219-
let action = AccountAction.synchronizeSites { result in
220+
let action = AccountAction.synchronizeSites(selectedSiteID: nil) { result in
220221
promise(result)
221222
}
222223
store.onAction(action)
@@ -236,7 +237,7 @@ class AccountStoreTests: XCTestCase {
236237

237238
// When
238239
let result: Result<Void, Error> = waitFor { promise in
239-
let action = AccountAction.synchronizeSites { result in
240+
let action = AccountAction.synchronizeSites(selectedSiteID: nil) { result in
240241
promise(result)
241242
}
242243
store.onAction(action)
@@ -247,6 +248,58 @@ class AccountStoreTests: XCTestCase {
247248
XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Site.self), 2)
248249
}
249250

251+
/// Verifies that `synchronizeSites` deletes storage sites that do not exist remotely anymore.
252+
///
253+
func test_synchronizeSites_deletes_sites_that_do_not_exist_remotely() {
254+
// Given
255+
let store = AccountStore(dispatcher: dispatcher, storageManager: storageManager, network: network)
256+
let siteIDInStorageOnly = Int64(127)
257+
storageManager.insertSampleSite(readOnlySite: Site.fake().copy(siteID: siteIDInStorageOnly))
258+
network.simulateResponse(requestUrlSuffix: "me/sites", filename: "sites")
259+
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Site.self), 1)
260+
XCTAssertNotNil(viewStorage.loadSite(siteID: siteIDInStorageOnly))
261+
262+
// When
263+
let result: Result<Void, Error> = waitFor { promise in
264+
let action = AccountAction.synchronizeSites(selectedSiteID: nil) { result in
265+
promise(result)
266+
}
267+
store.onAction(action)
268+
}
269+
270+
// Then
271+
XCTAssertTrue(result.isSuccess)
272+
// `sites.json` contains 2 sites that do not match `siteIDInStorageOnly`.
273+
XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Site.self), 2)
274+
XCTAssertNil(viewStorage.loadSite(siteID: siteIDInStorageOnly))
275+
}
276+
277+
/// Verifies that `synchronizeSites` does not delete selected site after syncing and the selected site does not exist remotely anymore.
278+
///
279+
func test_synchronizeSites_does_not_delete_selected_site_that_does_not_exist_remotely() {
280+
// Given
281+
let store = AccountStore(dispatcher: dispatcher, storageManager: storageManager, network: network)
282+
let selectedSiteID = Int64(127)
283+
storageManager.insertSampleSite(readOnlySite: Site.fake().copy(siteID: selectedSiteID))
284+
network.simulateResponse(requestUrlSuffix: "me/sites", filename: "sites")
285+
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.Site.self), 1)
286+
XCTAssertNotNil(viewStorage.loadSite(siteID: selectedSiteID))
287+
288+
// When
289+
let result: Result<Void, Error> = waitFor { promise in
290+
let action = AccountAction.synchronizeSites(selectedSiteID: selectedSiteID) { result in
291+
promise(result)
292+
}
293+
store.onAction(action)
294+
}
295+
296+
// Then
297+
XCTAssertTrue(result.isSuccess)
298+
// `sites.json` contains 2 sites that do not match `siteIDInStorageOnly`.
299+
XCTAssertEqual(self.viewStorage.countObjects(ofType: Storage.Site.self), 3)
300+
XCTAssertNotNil(viewStorage.loadSite(siteID: selectedSiteID))
301+
}
302+
250303
// MARK: - AccountAction.loadAccount
251304

252305
func test_loadAccount_returns_expected_account() {

0 commit comments

Comments
 (0)