Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MBL-1814] PLOT plan selector component #2195

Merged
merged 28 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
fe44ad0
PLOT Plan selector - Default state
jovaniks Nov 12, 2024
62e2ae0
Enum caps
jovaniks Nov 12, 2024
b973413
Files formated
jovaniks Nov 12, 2024
c207ab4
Adding test cases
jovaniks Nov 12, 2024
1d3a34c
More unit tests
jovaniks Nov 13, 2024
f6aa2e7
Added config function and Unit tests
jovaniks Nov 13, 2024
bfb8657
Implementing the config function to the PledgePaymentPlansViewControl…
jovaniks Nov 13, 2024
318a39d
Fixing the checkmark style to follow the figma designs
jovaniks Nov 13, 2024
27a3345
Remove Prelude and create helper functions instead. Reuse the `Pledge…
jovaniks Nov 15, 2024
152587c
Format
jovaniks Nov 15, 2024
1343f23
Fix PledgePaymentPlansDataSourceTest
jovaniks Nov 15, 2024
4ca06c3
Merge branch 'main' into jluna/MBL-1814/plot-plan-selector-component
jovaniks Nov 19, 2024
bfeb43f
Refactor: using UIStackView instead of a UITableView [MBL-1906]
jovaniks Nov 26, 2024
28ddf83
Merge branch 'jluna/MBL-1814/plot-plan-selector-component' of github.…
jovaniks Nov 26, 2024
5225a76
Merge remote-tracking branch 'oss/main' into jluna/MBL-1814/plot-plan…
jovaniks Nov 26, 2024
16830c7
Fix tests
jovaniks Nov 26, 2024
fc6ac56
PR feedback
jovaniks Nov 26, 2024
949dc63
PR Feedback
jovaniks Nov 26, 2024
1030358
Merge branch 'main' into jluna/MBL-1814/plot-plan-selector-component
jovaniks Nov 26, 2024
3d22914
PR Feedback
jovaniks Nov 26, 2024
54a8669
Restoring snapshots
jovaniks Nov 26, 2024
1259d5f
Fix stack view spacing
jovaniks Nov 26, 2024
a01864e
Fixing tests
jovaniks Nov 26, 2024
54109ce
Fix test
jovaniks Nov 26, 2024
a197ee6
Refactoring PledgePaymentPlanOptionView removing unnecessary stackviews.
jovaniks Dec 2, 2024
64e6789
Update tests snapshots
jovaniks Dec 2, 2024
44073f0
Fix snapshots
jovaniks Dec 2, 2024
56c0173
Removing PledgeDisclaimerView fix. They will be done in a new PR
jovaniks Dec 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Library
import UIKit

protocol PledgePaymentPlansViewControllerDelegate: AnyObject {
func pledgePaymentPlansViewController(
_ viewController: PledgePaymentPlansViewController,
didSelectPaymentPlan paymentPlan: PledgePaymentPlansType
)
}

final class PledgePaymentPlansViewController: UIViewController {
// MARK: Properties

private lazy var rootStackView: UIStackView = { UIStackView(frame: .zero) }()

private lazy var separatorView: UIView = { UIView(frame: .zero) }()

private lazy var pledgeInFullOption = PledgePaymentPlanOptionView(frame: .zero)
private lazy var pledgeOverTimeOption = PledgePaymentPlanOptionView(frame: .zero)

internal weak var delegate: PledgePaymentPlansViewControllerDelegate?

private let viewModel: PledgePaymentPlansViewModelType = PledgePaymentPlansViewModel()

// MARK: Lifecycle

override func viewDidLoad() {
super.viewDidLoad()

self.configureSubviews()
self.setupConstraints()

self.viewModel.inputs.viewDidLoad()
}

private func configureSubviews() {
self.view.addSubview(self.rootStackView)

self.pledgeInFullOption.delegate = self
self.pledgeOverTimeOption.delegate = self

self.rootStackView.addArrangedSubviews([
self.pledgeInFullOption,
self.separatorView,
self.pledgeOverTimeOption
])
}

private func setupConstraints() {
self.rootStackView.translatesAutoresizingMaskIntoConstraints = false

NSLayoutConstraint.activate([
self.rootStackView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
self.rootStackView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
self.rootStackView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.rootStackView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
self.separatorView.heightAnchor.constraint(equalToConstant: 0.5)
])
}

// MARK: - Bind Styles

override func bindStyles() {
super.bindStyles()

applyRootStackViewStyle(self.rootStackView)

applyWhiteBackgroundStyle(self.view)

applySeparatorStyle(self.separatorView)
}

// MARK: - View model

override func bindViewModel() {
super.bindViewModel()

self.viewModel.outputs.reloadPaymentPlans
.observeForUI()
.observeValues { [weak self] data in
guard let self = self else { return }

self.pledgeInFullOption.configureWith(value: PledgePaymentPlanOptionData(
type: .pledgeInFull,
selectedType: data.selectedPlan
))
self.pledgeOverTimeOption.configureWith(value: PledgePaymentPlanOptionData(
type: .pledgeOverTime,
selectedType: data.selectedPlan
))
}

self.viewModel.outputs.notifyDelegatePaymentPlanSelected
.observeForUI()
.observeValues { [weak self] paymentPlan in
guard let self = self else { return }

self.delegate?.pledgePaymentPlansViewController(self, didSelectPaymentPlan: paymentPlan)
}
}

// MARK: - Configuration

func configure(with value: PledgePaymentPlansAndSelectionData) {
self.viewModel.inputs.configure(with: value)
}
}

// MARK: - UITableViewDelegate

extension PledgePaymentPlansViewController: PledgePaymentPlanOptionViewDelegate {
func pledgePaymentPlanOptionView(
_: PledgePaymentPlanOptionView,
didSelectPlanType paymentPlanType: PledgePaymentPlansType
) {
self.viewModel.inputs.didSelectPlanType(paymentPlanType)
}
}

// MARK: Styles

private func applyRootStackViewStyle(_ stackView: UIStackView) {
stackView.axis = .vertical
stackView.spacing = 0
}

private func applyWhiteBackgroundStyle(_ view: UIView) {
view.backgroundColor = UIColor.ksr_white
}

private func applySeparatorStyle(_ view: UIView) {
view.backgroundColor = UIColor.ksr_support_300
view.accessibilityElementsHidden = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
@testable import Kickstarter_Framework
@testable import Library
import Prelude
import SnapshotTesting
import UIKit

final class PledgePaymentPlansViewControllerTest: TestCase {
override func setUp() {
super.setUp()
AppEnvironment.pushEnvironment(mainBundle: Bundle.framework)
UIView.setAnimationsEnabled(false)
}

override func tearDown() {
AppEnvironment.popEnvironment()
UIView.setAnimationsEnabled(true)

super.tearDown()
}

func testView_PledgeInFullSelected() {
orthogonalCombos([Language.en], [Device.pad, Device.phone4_7inch]).forEach { language, device in
withEnvironment(language: language) {
let controller = PledgePaymentPlansViewController.instantiate()

let data = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeInFull)
controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

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

func testView_PledgeOverTimeSelected() {
orthogonalCombos([Language.en], [Device.pad, Device.phone4_7inch]).forEach { language, device in
withEnvironment(language: language) {
let controller = PledgePaymentPlansViewController.instantiate()

let data = PledgePaymentPlansAndSelectionData(selectedPlan: .pledgeOverTime)
controller.configure(with: data)

let (parent, _) = traitControllers(device: device, orientation: .portrait, child: controller)
parent.view.frame.size.height = 400

self.scheduler.advance(by: .seconds(1))

assertSnapshot(matching: parent.view, as: .image, named: "lang_\(language)_device_\(device)")
}
}
}
}
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.
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import Library
import UIKit

protocol PledgePaymentPlanOptionViewDelegate: AnyObject {
func pledgePaymentPlanOptionView(
_ optionView: PledgePaymentPlanOptionView,
didSelectPlanType paymentPlanType: PledgePaymentPlansType
)
}

final class PledgePaymentPlanOptionView: UIView {
// MARK: - Properties
private lazy var contentView: UIView = UIView(frame: .zero)
private lazy var optionDescriptorStackView: UIStackView = { UIStackView(frame: .zero) }()
private lazy var titleLabel = { UILabel(frame: .zero) }()
private lazy var subtitleLabel = { UILabel(frame: .zero) }()
private lazy var selectionIndicatorImageView: UIImageView = { UIImageView(frame: .zero) }()

private let viewModel: PledgePaymentPlansOptionViewModelType = PledgePaymentPlansOptionViewModel()

public weak var delegate: PledgePaymentPlanOptionViewDelegate?

override init(frame: CGRect) {
super.init(frame: frame)

self.bindViewModel()
self.configureSubviews()
self.setupConstraints()
self.configureTapGesture()
}

@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

// MARK: - Configuration

private func configureSubviews() {

self.addSubview(self.contentView)

self.contentView.addSubview(self.selectionIndicatorImageView)
self.contentView.addSubview(self.optionDescriptorStackView)

self.optionDescriptorStackView.addArrangedSubviews([self.titleLabel, self.subtitleLabel])
}

private func setupConstraints() {
self.contentView.translatesAutoresizingMaskIntoConstraints = false
self.selectionIndicatorImageView.translatesAutoresizingMaskIntoConstraints = false
self.optionDescriptorStackView.translatesAutoresizingMaskIntoConstraints = false

self.titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
self.titleLabel.setContentHuggingPriority(.required, for: .vertical)

self.subtitleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
self.subtitleLabel.setContentHuggingPriority(.required, for: .vertical)

NSLayoutConstraint.activate([
self.contentView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: Styles.grid(2)),
self.contentView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -Styles.grid(2)),
self.contentView.topAnchor.constraint(equalTo: self.topAnchor, constant: Styles.grid(2)),
self.contentView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -Styles.grid(2))
])

NSLayoutConstraint.activate([
self.optionDescriptorStackView.leadingAnchor.constraint(equalTo: self.selectionIndicatorImageView.trailingAnchor, constant: Styles.grid(2)),
self.optionDescriptorStackView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
self.optionDescriptorStackView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
self.optionDescriptorStackView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor)
])

NSLayoutConstraint.activate([
self.selectionIndicatorImageView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
self.selectionIndicatorImageView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
self.selectionIndicatorImageView.widthAnchor.constraint(equalToConstant: Styles.grid(4)),
self.selectionIndicatorImageView.heightAnchor.constraint(
equalTo: self.titleLabel.heightAnchor,
multiplier: 1.0
)
])
}

private func configureTapGesture() {
self.addGestureRecognizer(UITapGestureRecognizer(
target: self,
action: #selector(self.onOptionTapped)
))
}

// MARK: - Styles

override func bindStyles() {
super.bindStyles()

applyOptionDescriptorStackViewStyle(self.optionDescriptorStackView)
applyTitleLabelStyle(self.titleLabel)
applySubtitleLabelStyle(self.subtitleLabel)
applySelectionIndicatorImageViewStyle(self.selectionIndicatorImageView)
}

// MARK: - View model

override func bindViewModel() {
super.bindViewModel()

self.viewModel.outputs.selectionIndicatorImageName
.observeForUI()
.observeValues { [weak self] imageName in
self?.selectionIndicatorImageView.image = Library.image(named: imageName)
}

self.titleLabel.rac.text = self.viewModel.outputs.titleText

self.subtitleLabel.rac.text = self.viewModel.outputs.subtitleText
self.subtitleLabel.rac.hidden = self.viewModel.outputs.subtitleLabelHidden

self.viewModel.outputs.notifyDelegatePaymentPlanOptionSelected
.observeForUI()
.observeValues { [weak self] paymentPlan in
guard let self = self else { return }

self.delegate?.pledgePaymentPlanOptionView(self, didSelectPlanType: paymentPlan)
}
}

func configureWith(value: PledgePaymentPlanOptionData) {
self.viewModel.inputs.configureWith(data: value)
}

func refreshSelectedOption(_ selectedType: PledgePaymentPlansType) {
self.viewModel.inputs.refreshSelectedType(selectedType)
}

// MARK: - Actions

@objc private func onOptionTapped() {
self.viewModel.inputs.optionTapped()
}
}

// MARK: - Styles helper

private func applyOptionDescriptorStackViewStyle(_ stackView: UIStackView) {
stackView.axis = .vertical
stackView.spacing = Styles.grid(1)
}

private func applyTitleLabelStyle(_ label: UILabel) {
label.accessibilityTraits = UIAccessibilityTraits.header
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0
label.font = UIFont.ksr_subhead().bolded
}

private func applySubtitleLabelStyle(_ label: UILabel) {
label.accessibilityTraits = UIAccessibilityTraits.header
label.adjustsFontForContentSizeCategory = true
label.numberOfLines = 0
label.font = UIFont.ksr_caption1()
label.textColor = .ksr_support_400
}

private func applySelectionIndicatorImageViewStyle(_ imageView: UIImageView) {
imageView.contentMode = .center
}
Loading