Skip to content

Commit 732afde

Browse files
authored
Merge pull request #5938 from woocommerce/issue/5764-coupon-list-cell-UI
Coupons: Update list UI
2 parents f3c3df5 + 57ae49b commit 732afde

File tree

16 files changed

+333
-41
lines changed

16 files changed

+333
-41
lines changed

Networking/Networking/Mapper/CouponListMapper.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ 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
16-
.map { $0.copy(siteID: siteID) }
17-
.filter { $0.discountType != .other }
15+
return coupons.map { $0.copy(siteID: siteID) }
1816
}
1917
}
2018

Networking/NetworkingTests/Mapper/CouponListMapperTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class CouponListMapperTests: XCTestCase {
1111
///
1212
func test_CouponsList_map_parses_all_coupons_in_response() throws {
1313
let coupons = try mapLoadAllCouponsResponse()
14-
XCTAssertEqual(coupons.count, 3)
14+
XCTAssertEqual(coupons.count, 4)
1515
}
1616

1717
/// Verifies that the `siteID` is added in the mapper, to all results, because it's not provided by the API endpoint

Networking/NetworkingTests/Remote/CouponsRemoteTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class CouponsRemoteTests: XCTestCase {
4141
// Then
4242
XCTAssert(result.isSuccess)
4343
let coupons = try XCTUnwrap(result.get())
44-
XCTAssertEqual(coupons.count, 3)
44+
XCTAssertEqual(coupons.count, 4)
4545
}
4646

4747
/// Verifies that loadAllCoupons uses the passed in parameters to specify the page of results wanted.

WooCommerce/Classes/Extensions/UIImage+Woo.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ extension UIImage {
222222
.imageFlippedForRightToLeftLayoutDirection()
223223
}
224224

225+
/// Empty Coupons Icon
226+
///
227+
static var emptyCouponsImage: UIImage {
228+
return UIImage(named: "woo-empty-coupons")!
229+
}
230+
225231
/// Empty Products Icon
226232
///
227233
static var emptyProductsImage: UIImage {
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Foundation
2+
import Yosemite
3+
4+
extension Coupon.DiscountType {
5+
/// Localized name to be displayed for the discount type.
6+
///
7+
var localizedName: String {
8+
switch self {
9+
case .percent:
10+
return Localization.percentageDiscount
11+
case .fixedCart:
12+
return Localization.fixedCartDiscount
13+
case .fixedProduct:
14+
return Localization.fixedProductDiscount
15+
case .other:
16+
return Localization.otherDiscount
17+
}
18+
}
19+
20+
private enum Localization {
21+
static let percentageDiscount = NSLocalizedString("Percentage Discount", comment: "Name of percentage discount type")
22+
static let fixedCartDiscount = NSLocalizedString("Fixed Cart Discount", comment: "Name of fixed cart discount type")
23+
static let fixedProductDiscount = NSLocalizedString("Fixed Product Discount", comment: "Name of fixed product discount type")
24+
static let otherDiscount = NSLocalizedString("Other", comment: "Generic name of non-default discount types")
25+
}
26+
}
27+
28+
// MARK: - Coupon expiry status
29+
//
30+
extension Coupon {
31+
/// Expiry status for Coupons.
32+
///
33+
func expiryStatus(now: Date = Date()) -> ExpiryStatus {
34+
guard let expiryDate = dateExpires else {
35+
return .active
36+
}
37+
38+
guard let gmtTimeZone = TimeZone(identifier: "GMT") else {
39+
return .expired
40+
}
41+
42+
var calender = Calendar.current
43+
calender.timeZone = gmtTimeZone
44+
45+
let result = calender.compare(expiryDate, to: now, toGranularity: .day)
46+
return result == .orderedDescending ? .active : .expired
47+
}
48+
49+
enum ExpiryStatus {
50+
case active
51+
case expired
52+
53+
/// Localized name to be displayed for the expiry status.
54+
///
55+
var localizedName: String {
56+
switch self {
57+
case .active:
58+
return Localization.active
59+
case .expired:
60+
return Localization.expired
61+
}
62+
}
63+
64+
/// Background color for the expiry status label
65+
///
66+
var statusBackgroundColor: UIColor {
67+
switch self {
68+
case .active:
69+
return .withColorStudio(.green, shade: .shade5)
70+
case .expired:
71+
return .gray(.shade5)
72+
}
73+
}
74+
75+
private enum Localization {
76+
static let active = NSLocalizedString("Active", comment: "Status of coupons that are active")
77+
static let expired = NSLocalizedString("Expired", comment: "Status of coupons that are expired")
78+
}
79+
}
80+
}

WooCommerce/Classes/ViewRelated/Coupons/CouponListViewController.swift

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private extension CouponListViewController {
132132
}
133133

134134
func registerTableViewCells() {
135-
tableView.registerNib(for: TitleBodyTableViewCell.self)
135+
TitleAndSubtitleAndStatusTableViewCell.register(for: tableView)
136136
}
137137
}
138138

@@ -144,7 +144,7 @@ extension CouponListViewController {
144144
///
145145
func displayPlaceholderCoupons() {
146146
let options = GhostOptions(displaysSectionHeader: false,
147-
reuseIdentifier: TitleBodyTableViewCell.reuseIdentifier,
147+
reuseIdentifier: TitleAndSubtitleAndStatusTableViewCell.reuseIdentifier,
148148
rowsPerSection: Constants.placeholderRowsPerSection)
149149
tableView.displayGhostContent(options: options,
150150
style: .wooDefaultGhostStyle)
@@ -167,9 +167,9 @@ extension CouponListViewController {
167167
let emptyStateViewController = EmptyStateViewController(style: .list)
168168
let config = EmptyStateViewController.Config.withButton(
169169
message: .init(string: Localization.emptyStateMessage),
170-
image: .errorImage,
170+
image: .emptyCouponsImage,
171171
details: Localization.emptyStateDetails,
172-
buttonTitle: "") { _ in }
172+
buttonTitle: Localization.addCouponButton) { _ in }
173173

174174
displayEmptyStateViewController(emptyStateViewController)
175175
emptyStateViewController.configure(config)
@@ -211,19 +211,14 @@ extension CouponListViewController: UITableViewDataSource {
211211
}
212212

213213
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
214-
let cell = tableView.dequeueReusableCell(withIdentifier: TitleBodyTableViewCell.reuseIdentifier, for: indexPath)
215-
if let cellViewModel = viewModel.couponViewModels[safe: indexPath.row] {
216-
configure(cell as? TitleBodyTableViewCell, with: cellViewModel)
214+
let cell = tableView.dequeueReusableCell(withIdentifier: TitleAndSubtitleAndStatusTableViewCell.reuseIdentifier, for: indexPath)
215+
if let cellViewModel = viewModel.couponViewModels[safe: indexPath.row],
216+
let cell = cell as? TitleAndSubtitleAndStatusTableViewCell {
217+
cell.configureCell(viewModel: cellViewModel)
217218
}
218219

219220
return cell
220221
}
221-
222-
func configure(_ cell: TitleBodyTableViewCell?, with cellViewModel: CouponListCellViewModel) {
223-
cell?.titleLabel.text = cellViewModel.title
224-
cell?.bodyLabel.text = cellViewModel.subtitle
225-
cell?.accessibilityLabel = cellViewModel.accessibilityLabel
226-
}
227222
}
228223

229224

@@ -246,11 +241,13 @@ private extension CouponListViewController {
246241
comment: "Coupon management coupon list screen title")
247242

248243
static let emptyStateMessage = NSLocalizedString(
249-
"No coupons yet",
250-
comment: "The text on the placeholder overlay when there are no coupons on the coupon management list")
244+
"Everyone loves a deal",
245+
comment: "The title on the placeholder overlay when there are no coupons on the coupon list screen.")
251246

252247
static let emptyStateDetails = NSLocalizedString(
253-
"Market your products by adding a coupon to offer your customers a discount.",
254-
comment: "The details on the placeholder overlay when there are no coupons on the coupon management list")
248+
"Boost your business by sending customers special offers and discounts.",
249+
comment: "The description on the placeholder overlay when there are no coupons on the coupon list screen.")
250+
251+
static let addCouponButton = NSLocalizedString("Add Coupon", comment: "Title for the action button to add coupon on the coupon list screen.")
255252
}
256253
}

WooCommerce/Classes/ViewRelated/Coupons/CouponManagementListViewModel.swift renamed to WooCommerce/Classes/ViewRelated/Coupons/CouponListViewModel.swift

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,7 @@ import Combine
22
import Yosemite
33
import protocol Storage.StorageManagerType
44
import class AutomatticTracks.CrashLogging
5-
6-
7-
struct CouponListCellViewModel {
8-
var title: String
9-
var subtitle: String
10-
var accessibilityLabel: String
11-
}
5+
import UIKit
126

137
enum CouponListState {
148
case initialized // ViewModel ready to recieve actions
@@ -21,6 +15,8 @@ enum CouponListState {
2115

2216
final class CouponListViewModel {
2317

18+
typealias CouponListCellViewModel = TitleAndSubtitleAndStatusTableViewCell.ViewModel
19+
2420
/// Active state
2521
///
2622
@Published private(set) var state: CouponListState = .initialized
@@ -97,11 +93,13 @@ final class CouponListViewModel {
9793
}
9894

9995
func buildCouponViewModels() {
100-
couponViewModels = resultsController.fetchedObjects.map({ coupon in
101-
return CouponListCellViewModel(title: coupon.code,
102-
subtitle: coupon.description,
103-
accessibilityLabel: coupon.description)
104-
})
96+
couponViewModels = resultsController.fetchedObjects.map { coupon in
97+
CouponListCellViewModel(title: coupon.code,
98+
subtitle: coupon.discountType.localizedName, // to be updated after UI is finalized
99+
accessibilityLabel: coupon.description.isEmpty ? coupon.description : coupon.code,
100+
status: coupon.expiryStatus().localizedName,
101+
statusBackgroundColor: coupon.expiryStatus().statusBackgroundColor)
102+
}
105103
}
106104

107105

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import UIKit
2+
3+
final class TitleAndSubtitleAndStatusTableViewCell: UITableViewCell {
4+
5+
@IBOutlet private var subtitleLabel: UILabel!
6+
@IBOutlet private var titleLabel: UILabel!
7+
@IBOutlet private var statusContainerView: UIView!
8+
@IBOutlet private var statusLabel: PaddedLabel!
9+
10+
static func register(for tableView: UITableView) {
11+
tableView.registerNib(for: self)
12+
}
13+
14+
override func awakeFromNib() {
15+
super.awakeFromNib()
16+
configureBackground()
17+
configureLabels()
18+
}
19+
20+
func configureCell(viewModel: ViewModel) {
21+
titleLabel.text = viewModel.title
22+
subtitleLabel.text = viewModel.subtitle
23+
accessibilityLabel = viewModel.accessibilityLabel
24+
statusLabel.text = viewModel.status
25+
statusLabel.backgroundColor = viewModel.statusBackgroundColor
26+
}
27+
28+
}
29+
30+
// MARK: - CellViewModel subtype
31+
//
32+
extension TitleAndSubtitleAndStatusTableViewCell {
33+
struct ViewModel {
34+
let title: String
35+
let subtitle: String
36+
let accessibilityLabel: String
37+
let status: String
38+
let statusBackgroundColor: UIColor
39+
}
40+
}
41+
42+
// MARK: - Setup
43+
//
44+
private extension TitleAndSubtitleAndStatusTableViewCell {
45+
func configureBackground() {
46+
backgroundColor = .listForeground
47+
selectedBackgroundView = UIView()
48+
selectedBackgroundView?.backgroundColor = .listBackground
49+
}
50+
51+
/// Setup: Labels
52+
///
53+
func configureLabels() {
54+
subtitleLabel.applyCaption1Style()
55+
titleLabel.applyBodyStyle()
56+
statusLabel.applyFootnoteStyle()
57+
statusLabel.numberOfLines = 0
58+
statusLabel.textColor = .black // constant because there will always background color on the label
59+
statusLabel.layer.cornerRadius = CGFloat(4.0)
60+
statusLabel.layer.masksToBounds = true
61+
}
62+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19455" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
3+
<device id="retina6_1" orientation="portrait" appearance="light"/>
4+
<dependencies>
5+
<deployment identifier="iOS"/>
6+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19454"/>
7+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
8+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
9+
</dependencies>
10+
<objects>
11+
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
12+
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
13+
<tableViewCell contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="TitleAndSubtitleAndStatusTableViewCell" rowHeight="90" id="KGk-i7-Jjw" customClass="TitleAndSubtitleAndStatusTableViewCell" customModule="WooCommerce" customModuleProvider="target">
14+
<rect key="frame" x="0.0" y="0.0" width="351" height="90"/>
15+
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
16+
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
17+
<rect key="frame" x="0.0" y="0.0" width="321.5" height="90"/>
18+
<autoresizingMask key="autoresizingMask"/>
19+
<subviews>
20+
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="top" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="zx0-L0-tSe">
21+
<rect key="frame" x="16" y="8" width="288" height="74"/>
22+
<subviews>
23+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="240" verticalHuggingPriority="260" text="Fixed product discount " textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kxr-JD-qZr" userLabel="Date Label">
24+
<rect key="frame" x="0.0" y="0.0" width="133.5" height="14.5"/>
25+
<fontDescription key="fontDescription" type="system" pointSize="12"/>
26+
<nil key="textColor"/>
27+
<nil key="highlightedColor"/>
28+
</label>
29+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="240" verticalHuggingPriority="260" text="Jan2022Sale" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="j1w-V3-ltS">
30+
<rect key="frame" x="0.0" y="20.5" width="99.5" height="20.5"/>
31+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
32+
<nil key="textColor"/>
33+
<nil key="highlightedColor"/>
34+
</label>
35+
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="y5e-MD-t6e">
36+
<rect key="frame" x="0.0" y="47" width="229" height="27"/>
37+
<subviews>
38+
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="260" verticalHuggingPriority="240" verticalCompressionResistancePriority="749" text="Active" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VXC-Oy-j48" customClass="PaddedLabel" customModule="WooCommerce" customModuleProvider="target">
39+
<rect key="frame" x="0.0" y="0.0" width="47.5" height="27"/>
40+
<fontDescription key="fontDescription" type="system" pointSize="17"/>
41+
<nil key="textColor"/>
42+
<nil key="highlightedColor"/>
43+
</label>
44+
</subviews>
45+
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
46+
<constraints>
47+
<constraint firstAttribute="bottom" secondItem="VXC-Oy-j48" secondAttribute="bottom" id="4RM-1O-ANI"/>
48+
<constraint firstItem="VXC-Oy-j48" firstAttribute="leading" secondItem="y5e-MD-t6e" secondAttribute="leading" id="Gq8-Ua-6Y2"/>
49+
<constraint firstItem="VXC-Oy-j48" firstAttribute="top" secondItem="y5e-MD-t6e" secondAttribute="top" id="PZ0-hh-rII"/>
50+
</constraints>
51+
</view>
52+
</subviews>
53+
</stackView>
54+
</subviews>
55+
<constraints>
56+
<constraint firstAttribute="trailing" secondItem="zx0-L0-tSe" secondAttribute="trailing" constant="16" id="BTB-Ao-unH"/>
57+
<constraint firstItem="zx0-L0-tSe" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="8" id="Dp8-IQ-gtb"/>
58+
<constraint firstAttribute="bottom" secondItem="zx0-L0-tSe" secondAttribute="bottom" constant="8" id="MOE-Q0-T7x"/>
59+
<constraint firstItem="zx0-L0-tSe" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="16" id="Vsm-y7-zbr"/>
60+
</constraints>
61+
</tableViewCellContentView>
62+
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
63+
<connections>
64+
<outlet property="statusContainerView" destination="y5e-MD-t6e" id="jLw-8w-mDZ"/>
65+
<outlet property="statusLabel" destination="VXC-Oy-j48" id="ihu-Fx-m1e"/>
66+
<outlet property="subtitleLabel" destination="Kxr-JD-qZr" id="Dqc-XN-M1M"/>
67+
<outlet property="titleLabel" destination="j1w-V3-ltS" id="9i7-hQ-oZV"/>
68+
</connections>
69+
<point key="canvasLocation" x="157" y="117"/>
70+
</tableViewCell>
71+
</objects>
72+
</document>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"images" : [
3+
{
4+
"filename" : "woo-empty-coupons.pdf",
5+
"idiom" : "universal"
6+
},
7+
{
8+
"appearances" : [
9+
{
10+
"appearance" : "luminosity",
11+
"value" : "dark"
12+
}
13+
],
14+
"filename" : "img-coupons-dark.pdf",
15+
"idiom" : "universal"
16+
}
17+
],
18+
"info" : {
19+
"author" : "xcode",
20+
"version" : 1
21+
}
22+
}

0 commit comments

Comments
 (0)