Skip to content

Commit

Permalink
[MBL-1720] Handle estimated shipping on the manage pledge screen (#2154)
Browse files Browse the repository at this point in the history
* Show estimated shipping

* Update manage pledge tests

* Update rewardReceivedView tests

* Fix viewmodel tests

* Turn on flag in relevant tests
  • Loading branch information
ifosli authored Sep 11, 2024
1 parent 6976e5f commit 579dc69
Show file tree
Hide file tree
Showing 89 changed files with 205 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ final class ManagePledgeViewControllerTests: TestCase {
fetchProjectRewardsResult: .success([reward])
)

combos(Language.allLanguages, [Device.phone4_7inch, Device.pad]).forEach { language, device in
orthogonalCombos(Language.allLanguages, [Device.phone4_7inch, Device.pad]).forEach { language, device in
withEnvironment(apiService: mockService, currentUser: user, language: language) {
let controller = ManagePledgeViewController.instantiate()
controller.configureWith(params: (Param.slug("project-slug"), Param.id(1)))
Expand Down Expand Up @@ -278,7 +278,7 @@ final class ManagePledgeViewControllerTests: TestCase {
fetchProjectRewardsResult: .success([reward])
)

combos(Language.allLanguages, Device.allCases).forEach { language, device in
orthogonalCombos(Language.allLanguages, Device.allCases).forEach { language, device in
withEnvironment(apiService: mockService, currentUser: user, language: language) {
let controller = ManagePledgeViewController.instantiate()
controller.configureWith(params: (Param.slug("project-slug"), Param.id(1)))
Expand Down Expand Up @@ -339,7 +339,7 @@ final class ManagePledgeViewControllerTests: TestCase {
fetchProjectRewardsResult: .success([reward])
)

combos(Language.allLanguages, [Device.phone4_7inch, Device.pad]).forEach { language, device in
orthogonalCombos(Language.allLanguages, [Device.phone4_7inch, Device.pad]).forEach { language, device in
withEnvironment(apiService: mockService, currentUser: user, language: language) {
let controller = ManagePledgeViewController.instantiate()
controller.configureWith(params: (Param.slug("project-slug"), Param.id(1)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ final class ManageViewPledgeRewardReceivedViewController: UIViewController {
= ManageViewPledgeRewardReceivedViewModel()

private lazy var rootStackView: UIStackView = { UIStackView(frame: .zero) }()
private lazy var titleLabel: UILabel = { UILabel(frame: .zero) }()
private lazy var labelStackView: UIStackView = { UIStackView(frame: .zero) }()
private lazy var deliveryLabel: UILabel = { UILabel(frame: .zero) }()
private lazy var shippingLabel: UILabel = { UILabel(frame: .zero) }()
private lazy var toggleViewController: ToggleViewController = {
ToggleViewController(nibName: nil, bundle: nil)
}()
Expand Down Expand Up @@ -38,7 +40,10 @@ final class ManageViewPledgeRewardReceivedViewController: UIViewController {
|> ksr_addSubviewToParent()
|> ksr_constrainViewToEdgesInParent()

_ = ([self.titleLabel, self.toggleViewController.view], self.rootStackView)
_ = ([self.deliveryLabel, self.shippingLabel], self.labelStackView)
|> ksr_addArrangedSubviewsToStackView()

_ = ([self.labelStackView, self.toggleViewController.view], self.rootStackView)
|> ksr_addArrangedSubviewsToStackView()
}

Expand Down Expand Up @@ -68,6 +73,11 @@ final class ManageViewPledgeRewardReceivedViewController: UIViewController {
|> \.spacing .~ Styles.grid(1)
|> \.insetsLayoutMarginsFromSafeArea .~ false

self.labelStackView.isLayoutMarginsRelativeArrangement = true
self.labelStackView.axis = .vertical
self.labelStackView.spacing = Styles.grid(2)
self.labelStackView.insetsLayoutMarginsFromSafeArea = false

_ = self.toggleViewController.titleLabel
|> checkoutTitleLabelStyle
|> \.font .~ UIFont.ksr_subhead()
Expand All @@ -84,7 +94,9 @@ final class ManageViewPledgeRewardReceivedViewController: UIViewController {
override func bindViewModel() {
super.bindViewModel()

self.titleLabel.rac.attributedText = self.viewModel.outputs.estimatedDeliveryDateLabelAttributedText
self.deliveryLabel.rac.attributedText = self.viewModel.outputs.estimatedDeliveryDateLabelAttributedText
self.shippingLabel.rac.attributedText = self.viewModel.outputs.estimatedShippingAttributedText
self.shippingLabel.rac.hidden = self.viewModel.outputs.estimatedShippingHidden
self.toggleViewController.toggle.rac.on = self.viewModel.outputs.rewardReceived
self.toggleViewController.view.rac.hidden = self.viewModel.outputs.rewardReceivedHidden

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ final class ManageViewPledgeRewardReceivedViewControllerTests: TestCase {
project: .template,
backerCompleted: false,
estimatedDeliveryOn: 1_475_361_315,
backingState: .collected
backingState: .collected,
estimatedShipping: nil
)

let devices = [Device.phone4_7inch, Device.phone5_8inch, Device.pad]
combos(Language.allLanguages, devices).forEach { language, device in
orthogonalCombos(Language.allLanguages, devices).forEach { language, device in
withEnvironment(language: language) {
let controller = ManageViewPledgeRewardReceivedViewController.instantiate()
controller.configureWith(data: data)
Expand All @@ -50,13 +51,14 @@ final class ManageViewPledgeRewardReceivedViewControllerTests: TestCase {
func testView_Toggle_On() {
let data = ManageViewPledgeRewardReceivedViewData(
project: .template,
backerCompleted: false,
backerCompleted: true,
estimatedDeliveryOn: 1_475_361_315,
backingState: .collected
backingState: .collected,
estimatedShipping: nil
)

let devices = [Device.phone4_7inch, Device.phone5_8inch, Device.pad]
combos(Language.allLanguages, devices).forEach { language, device in
orthogonalCombos(Language.allLanguages, devices).forEach { language, device in
withEnvironment(language: language) {
let controller = ManageViewPledgeRewardReceivedViewController.instantiate()
controller.configureWith(data: data)
Expand All @@ -73,4 +75,68 @@ final class ManageViewPledgeRewardReceivedViewControllerTests: TestCase {
}
}
}

func testEstimatedShipping_NoToggle() {
let data = ManageViewPledgeRewardReceivedViewData(
project: .template,
backerCompleted: false,
estimatedDeliveryOn: 1_475_361_315,
backingState: .pledged,
estimatedShipping: "About $3-$5"
)

let devices = [Device.phone4_7inch, Device.phone5_8inch, Device.pad]
let mockConfigClient = MockRemoteConfigClient()
mockConfigClient.features = [
RemoteConfigFeature.noShippingAtCheckout.rawValue: true
]
orthogonalCombos(Language.allLanguages, devices).forEach { language, device in
withEnvironment(language: language, remoteConfigClient: mockConfigClient) {
let controller = ManageViewPledgeRewardReceivedViewController.instantiate()
controller.configureWith(data: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)

parent.view.frame.size.height = 60

assertSnapshot(
matching: parent.view,
as: .image(perceptualPrecision: 0.98),
named: "lang_\(language)_device_\(device)"
)
}
}
}

func testEstimatedShipping_Toggle() {
let data = ManageViewPledgeRewardReceivedViewData(
project: .template,
backerCompleted: false,
estimatedDeliveryOn: 1_475_361_315,
backingState: .collected,
estimatedShipping: "About $10-$100"
)

let devices = [Device.phone4_7inch, Device.phone5_8inch, Device.pad]
let mockConfigClient = MockRemoteConfigClient()
mockConfigClient.features = [
RemoteConfigFeature.noShippingAtCheckout.rawValue: true
]
orthogonalCombos(Language.allLanguages, devices).forEach { language, device in
withEnvironment(language: language, remoteConfigClient: mockConfigClient) {
let controller = ManageViewPledgeRewardReceivedViewController.instantiate()
controller.configureWith(data: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)

parent.view.frame.size.height = 110

assertSnapshot(
matching: parent.view,
as: .image(perceptualPrecision: 0.98),
named: "lang_\(language)_device_\(device)"
)
}
}
}
}
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
71 changes: 63 additions & 8 deletions Library/ViewModels/ManagePledgeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,21 +226,76 @@ public final class ManagePledgeViewModel:
shouldBeginRefresh.ignoreValues()
)

// MARK: - ManageViewPledgeRewardReceivedViewData

let latestRewardDeliveryDate = self.loadProjectAndRewardsIntoDataSource.map { _, rewards in
rewards
.compactMap { $0.estimatedDeliveryOn }
.reduce(0) { accum, value in max(accum, value) }
}

self.configureRewardReceivedWithData = Signal.combineLatest(project, backing, latestRewardDeliveryDate)
.map { project, backing, latestRewardDeliveryDate in
ManageViewPledgeRewardReceivedViewData(
project: project,
backerCompleted: backing.backerCompleted ?? false,
estimatedDeliveryOn: latestRewardDeliveryDate,
backingState: backing.status
let baseReward = backing.map(\.reward).skipNil()

let shippingRules = Signal.combineLatest(project, baseReward.filter { $0.shipping.enabled })
.switchMap { project, reward in
AppEnvironment.current.apiService.fetchRewardShippingRules(
projectId: project.id,
rewardId: reward.id
)
.ksr_delay(AppEnvironment.current.apiDelayInterval, on: AppEnvironment.current.scheduler)
.map(ShippingRulesEnvelope.lens.shippingRules.view)
.retry(upTo: 3)
.materialize()
}
.values()

let shippingRule = Signal.combineLatest(backing, shippingRules)
.map { backing, shippingRules -> ShippingRule? in
guard let locationId = backing.locationId else { return nil }
return shippingRules.first(where: { $0.location.id == locationId })
}

let estimatedShipping = Signal.combineLatest(
shippingRule,
backing,
self.loadProjectAndRewardsIntoDataSource
)
.map { rule, backing, projectAndRewards -> String? in
let (project, rewards) = projectAndRewards
guard let rule,
let range = estimatedShippingText(
for: rewards,
project: project,
selectedShippingRule: rule,
selectedQuantities: selectedRewardQuantities(in: backing)
)
else { return nil }

return Strings.About_reward_amount(reward_amount: range)
}

let estimatedShippingAllRewards = Signal.merge(
estimatedShipping,
baseReward.filter { !$0.shipping.enabled }.mapConst(nil)
)

self.configureRewardReceivedWithData = Signal.combineLatest(
project,
backing,
latestRewardDeliveryDate,
estimatedShippingAllRewards
)
.map { project, backing, latestRewardDeliveryDate, estimatedShipping in
ManageViewPledgeRewardReceivedViewData(
project: project,
backerCompleted: backing.backerCompleted ?? false,
estimatedDeliveryOn: latestRewardDeliveryDate,
backingState: backing.status,
estimatedShipping: estimatedShipping
)
}

// MARK: - Menu options

let menuOptions = Signal.combineLatest(project, backing, userIsCreatorOfProject)
.map(actionSheetMenuOptionsFor(project:backing:userIsCreatorOfProject:))
Expand Down Expand Up @@ -560,7 +615,7 @@ private func managePledgeSummaryViewData(
projectState: project.state,
rewardMinimum: allRewardsTotal(for: backing),
shippingAmount: backing.shippingAmount.flatMap(Double.init),
shippingAmountHidden: backing.reward?.shipping.enabled == false,
shippingAmountHidden: backing.reward?.shipping.enabled == false || backing.shippingAmount == 0,
rewardIsLocalPickup: isRewardLocalPickup
)
}
Expand Down
15 changes: 5 additions & 10 deletions Library/ViewModels/ManagePledgeViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,8 @@ internal final class ManagePledgeViewModelTests: TestCase {
project: project,
backerCompleted: true,
estimatedDeliveryOn: 1_506_897_315.0,
backingState: .pledged
backingState: .pledged,
estimatedShipping: nil
)

withEnvironment(apiService: mockService) {
Expand Down Expand Up @@ -920,7 +921,8 @@ internal final class ManagePledgeViewModelTests: TestCase {
project: project,
backerCompleted: true,
estimatedDeliveryOn: 0,
backingState: .pledged
backingState: .pledged,
estimatedShipping: nil
)

withEnvironment(apiService: mockService1) {
Expand Down Expand Up @@ -974,14 +976,7 @@ internal final class ManagePledgeViewModelTests: TestCase {
self.loadProjectAndRewardsIntoDataSourceReward.assertValues([
[.noReward], [.noReward], [.noReward], [.noReward]
])
self.configureRewardReceivedWithData.assertValues([
expectedRewardReceivedData,
expectedRewardReceivedData,
expectedRewardReceivedData,
expectedRewardReceivedData,
expectedRewardReceivedData,
expectedRewardReceivedData
])
self.configureRewardReceivedWithData.assertLastValue(expectedRewardReceivedData)
self.title.assertValues(["Manage your pledge", "Manage your pledge", "Manage your pledge"])
}
}
Expand Down
27 changes: 24 additions & 3 deletions Library/ViewModels/ManageViewPledgeRewardReceivedViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public struct ManageViewPledgeRewardReceivedViewData: Equatable {
public let backerCompleted: Bool
public let estimatedDeliveryOn: TimeInterval
public let backingState: Backing.Status
public let estimatedShipping: String?
}

public protocol ManageViewPledgeRewardReceivedViewModelInputs {
Expand All @@ -19,6 +20,8 @@ public protocol ManageViewPledgeRewardReceivedViewModelInputs {
public protocol ManageViewPledgeRewardReceivedViewModelOutputs {
var cornerRadius: Signal<CGFloat, Never> { get }
var estimatedDeliveryDateLabelAttributedText: Signal<NSAttributedString, Never> { get }
var estimatedShippingAttributedText: Signal<NSAttributedString, Never> { get }
var estimatedShippingHidden: Signal<Bool, Never> { get }
var layoutMargins: Signal<UIEdgeInsets, Never> { get }
var marginWidth: Signal<CGFloat, Never> { get }
var rewardReceived: Signal<Bool, Never> { get }
Expand Down Expand Up @@ -78,6 +81,12 @@ public class ManageViewPledgeRewardReceivedViewModel:
self.estimatedDeliveryDateLabelAttributedText = data.map(\.estimatedDeliveryOn)
.map(estimatedDeliveryAttributedText)

self.estimatedShippingAttributedText = data.map(\.estimatedShipping).skipNil()
.map(formatEstimatedShipping)

self.estimatedShippingHidden = data.map(\.estimatedShipping)
.map { !featureNoShippingAtCheckout() || $0 == nil }

self.rewardReceivedHidden = data.map(\.backingState).map { state in state != .collected }
self.cornerRadius = self.rewardReceivedHidden.map { $0 ? 0 : Styles.grid(2) }
self.layoutMargins = self.rewardReceivedHidden.map { $0 ? .zero : .init(all: Styles.gridHalf(5)) }
Expand All @@ -101,6 +110,8 @@ public class ManageViewPledgeRewardReceivedViewModel:

public let cornerRadius: Signal<CGFloat, Never>
public let estimatedDeliveryDateLabelAttributedText: Signal<NSAttributedString, Never>
public let estimatedShippingAttributedText: Signal<NSAttributedString, Never>
public let estimatedShippingHidden: Signal<Bool, Never>
public let layoutMargins: Signal<UIEdgeInsets, Never>
public let marginWidth: Signal<CGFloat, Never>
public let rewardReceived: Signal<Bool, Never>
Expand All @@ -110,6 +121,12 @@ public class ManageViewPledgeRewardReceivedViewModel:
public var outputs: ManageViewPledgeRewardReceivedViewModelOutputs { return self }
}

private func formatEstimatedShipping(with shippingRange: String) -> NSAttributedString {
// In this UI, the title and value are simply separated by a space.
let totalString = "\(Strings.Estimated_Shipping()) \(shippingRange)"
return attributedText(totalString: totalString, valueSubstring: shippingRange)
}

private func estimatedDeliveryAttributedText(with date: TimeInterval) -> NSAttributedString {
let dateString = Format.date(
secondsInUTC: date,
Expand All @@ -118,15 +135,19 @@ private func estimatedDeliveryAttributedText(with date: TimeInterval) -> NSAttri
)
let string = Strings.backing_info_estimated_delivery_date(delivery_date: dateString)

return attributedText(totalString: string, valueSubstring: dateString)
}

private func attributedText(totalString: String, valueSubstring: String) -> NSAttributedString {
let font = UIFont.ksr_subhead()

let attributedText = NSMutableAttributedString(
attributedString: string
attributedString: totalString
.attributed(
with: font,
foregroundColor: .ksr_support_400,
attributes: [:],
bolding: [string.replacingOccurrences(of: dateString, with: "")]
bolding: [totalString.replacingOccurrences(of: valueSubstring, with: "")]
)
)

Expand All @@ -135,7 +156,7 @@ private func estimatedDeliveryAttributedText(with date: TimeInterval) -> NSAttri
.font: font,
.foregroundColor: UIColor.ksr_support_700
],
range: (string as NSString).range(of: dateString)
range: (totalString as NSString).range(of: valueSubstring)
)

return attributedText
Expand Down
Loading

0 comments on commit 579dc69

Please sign in to comment.