Skip to content

Commit fae3d49

Browse files
Decouple shipping location in interface between Rewards carousel and Location picker
This is another piece of prep work for MBL-2832.
1 parent aa4ad1d commit fae3d49

File tree

6 files changed

+171
-26
lines changed

6 files changed

+171
-26
lines changed

Kickstarter-iOS/Features/PledgeShippingLocation/PledgeShippingLocationViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import UIKit
88
protocol PledgeShippingLocationViewControllerDelegate: AnyObject {
99
func pledgeShippingLocationViewController(
1010
_ viewController: PledgeShippingLocationViewController,
11-
didSelect shippingRule: ShippingRule
11+
didSelect location: Location
1212
)
1313
func pledgeShippingLocationViewControllerLayoutDidUpdate(
1414
_ viewController: PledgeShippingLocationViewController,
@@ -128,7 +128,7 @@ final class PledgeShippingLocationViewController: UIViewController {
128128
.observeValues { [weak self] shippingRule in
129129
guard let self = self else { return }
130130

131-
self.delegate?.pledgeShippingLocationViewController(self, didSelect: shippingRule)
131+
self.delegate?.pledgeShippingLocationViewController(self, didSelect: shippingRule.location)
132132
}
133133

134134
self.viewModel.outputs.presentShippingRules

Kickstarter-iOS/Features/RewardsCollection/Controller/RewardsCollectionViewController.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ final class RewardsCollectionViewController: UICollectionViewController {
106106
)
107107

108108
self.setupConstraints()
109-
self.viewModel.inputs.shippingRuleSelected(nil)
109+
self.viewModel.inputs.shippingLocationSelected(nil)
110110

111111
self.viewModel.inputs.viewDidLoad()
112112
}
@@ -386,9 +386,9 @@ extension RewardsCollectionViewController: RewardCellDelegate {
386386
extension RewardsCollectionViewController: PledgeShippingLocationViewControllerDelegate {
387387
func pledgeShippingLocationViewController(
388388
_: PledgeShippingLocationViewController,
389-
didSelect shippingRule: ShippingRule
389+
didSelect location: Location
390390
) {
391-
self.viewModel.inputs.shippingRuleSelected(shippingRule)
391+
self.viewModel.inputs.shippingLocationSelected(location)
392392
}
393393

394394
func pledgeShippingLocationViewControllerLayoutDidUpdate(

Kickstarter-iOS/Features/RewardsCollection/Views/RewardCardContainerViewTests.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,12 @@ private func rewardCardInViewController(
447447
view.bottomAnchor.constraint(lessThanOrEqualTo: controller.view.layoutMarginsGuide.bottomAnchor)
448448
])
449449

450-
view.configure(with: (project: project, reward: reward, context: .pledge, currentShippingRule: nil))
450+
view.configure(with: RewardCardViewData(
451+
project: project,
452+
reward: reward,
453+
context: .pledge,
454+
currentShippingLocation: nil
455+
))
451456

452457
return parent
453458
}

Library/ViewModels/RewardCardViewModel.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public typealias RewardCardViewData = (
1919
project: Project,
2020
reward: Reward,
2121
context: RewardCardViewContext,
22-
currentShippingRule: ShippingRule?
22+
currentShippingLocation: Location?
2323
)
2424

2525
public protocol RewardCardViewModelInputs {
@@ -66,7 +66,7 @@ public final class RewardCardViewModel: RewardCardViewModelType, RewardCardViewM
6666

6767
let project: Signal<Project, Never> = configData.map(\.project)
6868
let reward: Signal<Reward, Never> = configData.map(\.reward)
69-
let currentShippingRule: Signal<ShippingRule?, Never> = configData.map(\.currentShippingRule)
69+
let currentShippingLocation: Signal<Location?, Never> = configData.map(\.currentShippingLocation)
7070

7171
let projectAndReward = Signal.zip(project, reward)
7272

@@ -134,9 +134,9 @@ public final class RewardCardViewModel: RewardCardViewModelType, RewardCardViewM
134134
self.rewardLocationStackViewHidden = reward
135135
.map { !isRewardLocalPickup($0) }
136136

137-
self.estimatedShippingLabelText = Signal.combineLatest(reward, project, currentShippingRule)
138-
.map { reward, project, shippingRule in
139-
guard let locationId = shippingRule?.location.id else { return nil }
137+
self.estimatedShippingLabelText = Signal.combineLatest(reward, project, currentShippingLocation)
138+
.map { reward, project, location in
139+
guard let locationId = location?.id else { return nil }
140140

141141
return estimatedShippingText(for: [reward], project: project, locationId: locationId)
142142
}

Library/ViewModels/RewardsCollectionViewModel.swift

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public protocol RewardsCollectionViewModelInputs {
2020
func rewardCellShouldShowDividerLine(_ show: Bool)
2121
func rewardSelected(with rewardId: Int)
2222
func shippingLocationViewDidFailToLoad()
23-
func shippingRuleSelected(_ shippingRule: ShippingRule?)
23+
func shippingLocationSelected(_ location: Location?)
2424
func traitCollectionDidChange(_ traitCollection: UITraitCollection)
2525
func viewDidAppear()
2626
func viewDidLayoutSubviews()
@@ -72,7 +72,7 @@ public final class RewardsCollectionViewModel: RewardsCollectionViewModelType,
7272
let rewards = project
7373
.map(allowableSortedProjectRewards)
7474

75-
let filteredByLocationRewards = Signal.combineLatest(rewards, self.shippingRuleSelectedSignal)
75+
let filteredByLocationRewards = Signal.combineLatest(rewards, self.shippingLocationSelectedSignal)
7676
.map(filteredRewardsByLocation)
7777

7878
self.title = configData
@@ -98,17 +98,23 @@ public final class RewardsCollectionViewModel: RewardsCollectionViewModelType,
9898
.skipNil()
9999
.take(first: 1)
100100

101+
let selectedShippingRule: Signal<ShippingRule?, Never> = self.selectedRewardProperty.signal.skipNil()
102+
.combineLatest(with: self.shippingLocationSelectedSignal.signal)
103+
.map { reward, location in
104+
shippingRule(forReward: reward, selectedLocation: location)
105+
}
106+
101107
self.reloadDataWithValues = Signal.combineLatest(
102108
project,
103109
rewards,
104110
filteredByLocationRewards,
105-
self.shippingRuleSelectedSignal.signal
111+
self.shippingLocationSelectedSignal.signal
106112
)
107-
.map { project, rewards, filteredByLocationRewards, shippingRule in
113+
.map { project, rewards, filteredByLocationRewards, location in
108114
if !filteredByLocationRewards.isEmpty {
109115
filteredByLocationRewards
110116
.filter { reward in isStartDateBeforeToday(for: reward) }
111-
.map { reward in (project, reward, .pledge, shippingRule) }
117+
.map { reward in (project, reward, .pledge, location) }
112118
} else {
113119
rewards
114120
.filter { reward in isStartDateBeforeToday(for: reward) }
@@ -155,7 +161,7 @@ public final class RewardsCollectionViewModel: RewardsCollectionViewModelType,
155161
project,
156162
selectedRewardFromId,
157163
refTag,
158-
self.shippingRuleSelectedSignal.signal
164+
selectedShippingRule
159165
)
160166
.takeWhen(self.rewardSelectedWithRewardIdProperty.signal)
161167
.filter { project, reward, _, _ in
@@ -402,9 +408,10 @@ public final class RewardsCollectionViewModel: RewardsCollectionViewModelType,
402408
self.shippingLocationViewDidFailToLoadProperty.value = ()
403409
}
404410

405-
private let (shippingRuleSelectedSignal, shippingRuleSelectedObserver) = Signal<ShippingRule?, Never>.pipe()
406-
public func shippingRuleSelected(_ shippingRule: ShippingRule?) {
407-
self.shippingRuleSelectedObserver.send(value: shippingRule)
411+
private let (shippingLocationSelectedSignal, shippingLocationSelectedObserver) = Signal<Location?, Never>
412+
.pipe()
413+
public func shippingLocationSelected(_ location: Location?) {
414+
self.shippingLocationSelectedObserver.send(value: location)
408415
}
409416

410417
private let traitCollectionChangedProperty = MutableProperty<UITraitCollection?>(nil)
@@ -555,9 +562,10 @@ private func allowableSortedProjectRewards(from project: Project) -> [Reward] {
555562

556563
private func filteredRewardsByLocation(
557564
_ rewards: [Reward],
558-
shippingRule: ShippingRule?
565+
location: Location?
559566
) -> [Reward] {
560567
return rewards.filter { reward in
568+
561569
var shouldDisplayReward = false
562570

563571
let isRewardLocalOrDigital = isRewardDigital(reward) || isRewardLocalPickup(reward)
@@ -570,7 +578,7 @@ private func filteredRewardsByLocation(
570578

571579
// If restricted shipping, compare against selected shipping location.
572580
} else if isRestrictedShippingReward {
573-
shouldDisplayReward = rewardShipsTo(selectedLocation: shippingRule?.location.id, reward)
581+
shouldDisplayReward = rewardShipsTo(selectedLocation: location?.id, reward)
574582
}
575583

576584
return shouldDisplayReward
@@ -592,3 +600,17 @@ private func rewardShipsTo(
592600

593601
return shippingLocationIds.contains(selectedLocationId)
594602
}
603+
604+
private func shippingRule(forReward reward: Reward, selectedLocation location: Location?) -> ShippingRule? {
605+
guard let rules = reward.shippingRulesExpanded else {
606+
let hasShipping = reward.isRestrictedShippingPreference || reward.isUnRestrictedShippingPreference
607+
assert(
608+
!hasShipping,
609+
"This reward is shippable, but no shipping rules were set. The backer may not be charged correctly for shipping."
610+
)
611+
612+
return nil
613+
}
614+
615+
return rules.first(where: { $0.location.id == location?.id })
616+
}

Library/ViewModels/RewardsCollectionViewModelTests.swift

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import Foundation
44
import Prelude
55
import ReactiveExtensions
66
import ReactiveExtensions_TestHelpers
7+
import XCTest
78

89
final class RewardsCollectionViewModelTests: TestCase {
910
private let reloadDataWithValues = TestObserver<[Reward], Never>()
1011
private let scrollToRewardIndex = TestObserver<Int, Never>()
12+
private let goToCustomizeYourReward = TestObserver<PledgeViewData, Never>()
1113

1214
private let vm = RewardsCollectionViewModel()
1315

@@ -18,6 +20,8 @@ final class RewardsCollectionViewModelTests: TestCase {
1820
.observe(self.reloadDataWithValues.observer)
1921

2022
self.vm.outputs.scrollToRewardIndexPath.map { $0.row }.observe(self.scrollToRewardIndex.observer)
23+
24+
self.vm.outputs.goToCustomizeYourReward.observe(self.goToCustomizeYourReward.observer)
2125
}
2226

2327
func testRewardsOrdered() {
@@ -37,7 +41,7 @@ final class RewardsCollectionViewModelTests: TestCase {
3741
|> Project.lens.rewardData.rewards .~ rewards
3842

3943
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: "34342")
40-
self.vm.shippingRuleSelected(nil)
44+
self.vm.shippingLocationSelected(nil)
4145
self.vm.viewDidLoad()
4246

4347
let rewardsOrdered = [
@@ -67,7 +71,7 @@ final class RewardsCollectionViewModelTests: TestCase {
6771
|> Project.lens.rewardData.rewards .~ rewards
6872

6973
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: "34342")
70-
self.vm.shippingRuleSelected(nil)
74+
self.vm.shippingLocationSelected(nil)
7175
self.vm.viewDidLoad()
7276
self.vm.viewDidLayoutSubviews()
7377

@@ -106,7 +110,7 @@ final class RewardsCollectionViewModelTests: TestCase {
106110
|> Project.lens.personalization.backing .~ backing
107111

108112
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: nil)
109-
self.vm.shippingRuleSelected(nil)
113+
self.vm.shippingLocationSelected(nil)
110114
self.vm.viewDidLoad()
111115
self.vm.viewDidLayoutSubviews()
112116

@@ -138,10 +142,124 @@ final class RewardsCollectionViewModelTests: TestCase {
138142
|> Project.lens.rewardData.rewards .~ rewards
139143

140144
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: nil)
141-
self.vm.shippingRuleSelected(nil)
145+
self.vm.shippingLocationSelected(nil)
142146
self.vm.viewDidLoad()
143147
self.vm.viewDidLayoutSubviews()
144148

145149
self.scrollToRewardIndex.assertDidNotEmitValue()
146150
}
151+
152+
func test_selectLocation_outputsShippingRule_forRewardWithShipping() {
153+
let location1 = Location(
154+
country: "Country 1",
155+
displayableName: "Country 1",
156+
id: 1,
157+
localizedName: "Country 1",
158+
name: "Country 1"
159+
)
160+
let location2 = Location(
161+
country: "Country 2",
162+
displayableName: "Country 2",
163+
id: 2,
164+
localizedName: "Country 2",
165+
name: "Country 2"
166+
)
167+
168+
let shippingRule1 = ShippingRule(
169+
cost: 10,
170+
id: 1,
171+
location: location1,
172+
estimatedMin: nil,
173+
estimatedMax: nil
174+
)
175+
let shippingRule2 = ShippingRule(
176+
cost: 72,
177+
id: 2,
178+
location: location2,
179+
estimatedMin: nil,
180+
estimatedMax: nil
181+
)
182+
183+
let reward = Reward.template
184+
|> Reward.lens.isAvailable .~ true
185+
|> Reward.lens.shippingRulesExpanded .~ [shippingRule1, shippingRule2]
186+
|> Reward.lens.shipping .~ Reward.Shipping(
187+
enabled: true,
188+
location: nil,
189+
preference: .restricted,
190+
summary: "Restricted shipping",
191+
type: .multipleLocations
192+
)
193+
194+
let rewards = [
195+
reward
196+
]
197+
198+
let testProject = Project.template
199+
|> Project.lens.rewardData.rewards .~ rewards
200+
201+
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: nil)
202+
self.vm.inputs.shippingLocationSelected(nil)
203+
self.vm.viewDidLoad()
204+
self.vm.viewDidLayoutSubviews()
205+
206+
self.vm.inputs.shippingLocationSelected(location2)
207+
self.vm.inputs.rewardSelected(with: reward.id)
208+
209+
self.goToCustomizeYourReward.assertDidEmitValue()
210+
211+
if let pledgeData = self.goToCustomizeYourReward.lastValue {
212+
XCTAssertEqual(
213+
pledgeData.selectedShippingRule,
214+
shippingRule2,
215+
"Pledge data should include shipping rule for location 2"
216+
)
217+
}
218+
}
219+
220+
func test_selectLocation_outputsNilShippingRule_forRewardWithoutShipping() {
221+
let location1 = Location(
222+
country: "Country 1",
223+
displayableName: "Country 1",
224+
id: 1,
225+
localizedName: "Country 1",
226+
name: "Country 1"
227+
)
228+
229+
let reward = Reward.template
230+
|> Reward.lens.isAvailable .~ true
231+
|> Reward.lens.shippingRulesExpanded .~ []
232+
|> Reward.lens.shipping .~ Reward.Shipping(
233+
enabled: true,
234+
location: nil,
235+
preference: Reward.Shipping.Preference.none,
236+
summary: "Digital reward",
237+
type: .noShipping
238+
)
239+
240+
let rewards = [
241+
reward
242+
]
243+
244+
let testProject = Project.template
245+
|> Project.lens.rewardData.rewards .~ rewards
246+
247+
self.vm.configure(with: testProject, refTag: nil, context: .createPledge, secretRewardToken: nil)
248+
self.vm.inputs.shippingLocationSelected(nil)
249+
self.vm.viewDidLoad()
250+
self.vm.viewDidLayoutSubviews()
251+
252+
self.vm.inputs.shippingLocationSelected(location1)
253+
self.vm.inputs.rewardSelected(with: reward.id)
254+
255+
self.goToCustomizeYourReward.assertDidEmitValue()
256+
257+
if let pledgeData = self.goToCustomizeYourReward.lastValue {
258+
XCTAssertEqual(
259+
pledgeData.selectedShippingRule,
260+
nil,
261+
"Pledge data should have no shipping rule, because the reward is digital"
262+
)
263+
}
264+
}
147265
}

0 commit comments

Comments
 (0)