Skip to content

Commit 97e0415

Browse files
authored
Merge pull request #5828 from woocommerce/issue/5767-load-next-page
Coupons: Add loading next page functionality to coupon list
2 parents 0b41fcc + fb699ac commit 97e0415

File tree

5 files changed

+88
-5
lines changed

5 files changed

+88
-5
lines changed

Networking/Networking/Mapper/CouponListMapper.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ struct CouponListMapper: Mapper {
1212
///
1313
func map(response: Data) throws -> [Coupon] {
1414
let coupons = try Coupon.decoder.decode(CouponListEnvelope.self, from: response).coupons
15-
return coupons.map { $0.copy(siteID: siteID) }
15+
return coupons
16+
.map { $0.copy(siteID: siteID) }
17+
.filter { $0.mappedDiscountType != nil }
1618
}
1719
}
1820

Networking/Networking/Model/Coupon.swift

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,22 @@ public struct Coupon {
2626
public let dateModified: Date
2727

2828
/// Determines the type of discount that will be applied. Options: `.percent` `.fixedCart` and `.fixedProduct`
29-
public let discountType: DiscountType
29+
public var discountType: DiscountType {
30+
if let type = mappedDiscountType {
31+
return type
32+
} else {
33+
// Returns default value for fallback case to avoid working with optionals.
34+
// Since `CouponListMapper` filters out nil `mappedDiscountType`,
35+
// this case is unlikely to happen.
36+
return .fixedCart
37+
}
38+
}
39+
40+
/// Discount type if matched with any of the ones supported by Core.
41+
/// Returns nil if other types are found.
42+
/// Used to filter only coupons with default types, so internal to this module only.
43+
///
44+
internal let mappedDiscountType: DiscountType?
3045

3146
public let description: String
3247

@@ -78,6 +93,9 @@ public struct Coupon {
7893
/// Email addresses of customers who have used this coupon
7994
public let usedBy: [String]
8095

96+
/// Discount types supported by Core.
97+
/// There are other types supported by other plugins, but those are not supported for now.
98+
///
8199
public enum DiscountType: String {
82100
case percent = "percent"
83101
case fixedCart = "fixed_cart"
@@ -114,7 +132,7 @@ public struct Coupon {
114132
self.amount = amount
115133
self.dateCreated = dateCreated
116134
self.dateModified = dateModified
117-
self.discountType = discountType
135+
self.mappedDiscountType = discountType
118136
self.description = description
119137
self.dateExpires = dateExpires
120138
self.usageCount = usageCount
@@ -148,7 +166,7 @@ extension Coupon: Codable {
148166
case amount
149167
case dateCreated = "dateCreatedGmt"
150168
case dateModified = "dateModifiedGmt"
151-
case discountType
169+
case mappedDiscountType
152170
case description
153171
case dateExpires = "dateExpiresGmt"
154172
case usageCount

WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ final class CouponListViewController: UIViewController {
1818
return refreshControl
1919
}()
2020

21+
/// Footer "Loading More" Spinner.
22+
///
23+
private lazy var footerSpinnerView = FooterSpinnerView()
24+
25+
/// Empty Footer Placeholder. Replaces spinner view and allows footer to collapse and be completely hidden.
26+
///
27+
private lazy var footerEmptyView = UIView(frame: .zero)
28+
2129
private var subscriptions: Set<AnyCancellable> = []
2230

2331
init(siteID: Int64) {
@@ -51,6 +59,8 @@ final class CouponListViewController: UIViewController {
5159
self.tableView.reloadData()
5260
case .refreshing:
5361
self.refreshControl.beginRefreshing()
62+
case .loadingNextPage:
63+
self.startFooterLoadingIndicator()
5464
case .initialized:
5565
break
5666
}
@@ -75,10 +85,33 @@ private extension CouponListViewController {
7585
func resetViews() {
7686
removeNoResultsOverlay()
7787
removePlaceholderCoupons()
88+
stopFooterLoadingIndicator()
7889
if refreshControl.isRefreshing {
7990
refreshControl.endRefreshing()
8091
}
8192
}
93+
94+
/// Starts the loading indicator in the footer, to show that another page is being fetched
95+
///
96+
func startFooterLoadingIndicator() {
97+
tableView?.tableFooterView = footerSpinnerView
98+
footerSpinnerView.startAnimating()
99+
}
100+
101+
/// Stops the loading indicator in the footer
102+
///
103+
func stopFooterLoadingIndicator() {
104+
footerSpinnerView.stopAnimating()
105+
tableView?.tableFooterView = footerEmptyView
106+
}
107+
}
108+
109+
// MARK: - TableView Delegate
110+
//
111+
extension CouponListViewController: UITableViewDelegate {
112+
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
113+
viewModel.tableWillDisplayCell(at: indexPath)
114+
}
82115
}
83116

84117

@@ -95,6 +128,7 @@ private extension CouponListViewController {
95128
tableView.estimatedRowHeight = Constants.estimatedRowHeight
96129
tableView.rowHeight = UITableView.automaticDimension
97130
tableView.addSubview(refreshControl)
131+
tableView.delegate = self
98132
}
99133

100134
func registerTableViewCells() {

WooCommerce/Classes/ViewRelated/Coupons/CouponManagementListViewModel.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ enum CouponListState {
1616
case empty // View should display the empty state
1717
case coupons // View should display the contents of `couponViewModels`
1818
case refreshing // View should display the refresh control
19+
case loadingNextPage // View should display a bottom loading indicator and contents of `couponViewModels`
1920
}
2021

2122
final class CouponListViewModel {
@@ -69,7 +70,7 @@ final class CouponListViewModel {
6970
storageManager: StorageManagerType) -> ResultsController<StorageCoupon> {
7071
let predicate = NSPredicate(format: "siteID == %lld", siteID)
7172
let descriptor = NSSortDescriptor(keyPath: \StorageCoupon.dateCreated,
72-
ascending: true)
73+
ascending: false)
7374

7475
return ResultsController<StorageCoupon>(storageManager: storageManager,
7576
matching: predicate,
@@ -123,6 +124,12 @@ final class CouponListViewModel {
123124
func refreshCoupons() {
124125
syncingCoordinator.resynchronize(reason: nil, onCompletion: nil)
125126
}
127+
128+
/// The ViewController can trigger loading of the next page when the user scrolls to the bottom
129+
///
130+
func tableWillDisplayCell(at indexPath: IndexPath) {
131+
syncingCoordinator.ensureNextPageIsSynchronized(lastVisibleIndex: indexPath.row)
132+
}
126133
}
127134

128135

@@ -172,6 +179,8 @@ private extension CouponListViewModel {
172179
func transitionToSyncingState(pageNumber: Int, hasData: Bool) {
173180
if pageNumber == 1 {
174181
state = hasData ? .refreshing : .loading
182+
} else {
183+
state = .loadingNextPage
175184
}
176185
}
177186

WooCommerce/WooCommerceTests/ViewRelated/Coupons/CouponListViewModelTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,24 @@ final class CouponListViewModelTests: XCTestCase {
145145
// Then
146146
XCTAssertEqual(sut.state, .coupons)
147147
}
148+
149+
func test_tableWillDisplayCellAtIndexPath_calls_ensureNextPageIsSynchronized_on_syncCoordinator() {
150+
// Given
151+
sut = CouponListViewModel(siteID: 123, syncingCoordinator: mockSyncingCoordinator)
152+
153+
// When
154+
sut.tableWillDisplayCell(at: IndexPath(row: 3, section: 0))
155+
156+
// Then
157+
XCTAssertTrue(mockSyncingCoordinator.spyDidCallEnsureNextPageIsSynchronized)
158+
XCTAssertEqual(mockSyncingCoordinator.spyEnsureNextPageIsSynchronizedLastVisibleIndex, 3)
159+
}
160+
161+
func test_sync_updates_state_correctly_when_syncing_next_page() {
162+
// When
163+
sut.sync(pageNumber: 2, pageSize: 10, reason: nil, onCompletion: nil)
164+
165+
// Then
166+
XCTAssertEqual(sut.state, .loadingNextPage)
167+
}
148168
}

0 commit comments

Comments
 (0)