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

Add promo badge for incentives in MPE #4285

Draft
wants to merge 6 commits into
base: tillh/mpe-promo-badge-ui
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@
C28450436BDA52BE9BE3BDC3 /* PaymentSheetConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E83494558F0C93C5B05A1DFB /* PaymentSheetConfigurationTests.swift */; };
C346B534D57A952D4415ADFD /* Intent+Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04C8047FD8994D3FAA3D1A7A /* Intent+Link.swift */; };
C5E3750BBCA700CF364F7578 /* PaymentSheetFormFactory+OXXO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F20379AE078D68A0AC83A6C5 /* PaymentSheetFormFactory+OXXO.swift */; };
CB225E962CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB225E952CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift */; };
CB46EF492CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */; };
CB46EF4B2CED1BDA00E9A7F2 /* PromoBadgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */; };
CD19725E26DBDB9960D828CB /* BottomSheetPresentationAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8F09CF961C943E36D76860F /* BottomSheetPresentationAnimator.swift */; };
CF2AD2C7F761C46AE559E563 /* SavedPaymentOptionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B3ECDF6CF9AABD573F86CA2 /* SavedPaymentOptionsViewController.swift */; };
D0B9FBCB359A7D774B98D19E /* LinkCookieKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1928BE9DFF116368B1A19DC /* LinkCookieKey.swift */; };
Expand Down Expand Up @@ -765,6 +768,9 @@
C90A2636C2A577AF36FB793B /* PaymentSheetLoaderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetLoaderTest.swift; sourceTree = "<group>"; };
C94104A367EAF6C8785C17A1 /* FormSpecProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormSpecProvider.swift; sourceTree = "<group>"; };
C9726902C985C99F69E6880C /* CustomerSheet+PaymentMethodAvailability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomerSheet+PaymentMethodAvailability.swift"; sourceTree = "<group>"; };
CB225E952CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodTypeCollectionViewCellSnapshotTests.swift; sourceTree = "<group>"; };
CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodIncentive.swift; sourceTree = "<group>"; };
CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoBadgeView.swift; sourceTree = "<group>"; };
CBCFE3D39D670C3C77C59722 /* cs-CZ */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "cs-CZ"; path = "cs-CZ.lproj/Localizable.strings"; sourceTree = "<group>"; };
CC3498CF4AEAA8F169616CDF /* STPCardBrandChoice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPCardBrandChoice.swift; sourceTree = "<group>"; };
CCA2B5817236F64A212A8C61 /* IntentConfirmParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentConfirmParams.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1041,6 +1047,7 @@
22552CD237A259249CD0C592 /* Intent.swift */,
CCA2B5817236F64A212A8C61 /* IntentConfirmParams.swift */,
9A0D887C5AC6EFFAFE1AFD77 /* PaymentMethodType.swift */,
CB46EF482CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift */,
2A19DBD87D0EBC7FA3DFB2A7 /* PaymentOption+Images.swift */,
6B680A2FF197F612D065F16C /* PaymentSheet.swift */,
5CD1A451B238C1D1ADAA72EC /* PaymentSheet+API.swift */,
Expand Down Expand Up @@ -1364,6 +1371,7 @@
B626EE922BF2872200B05B05 /* PaymentMethodTypeImageView.swift */,
383EA30FC9C862DF2217F96D /* PaymentSheetUIKitAdditions.swift */,
9356711AB2961A5F729F3EAA /* PayWithLinkButton.swift */,
CB46EF4A2CED1BDA00E9A7F2 /* PromoBadgeView.swift */,
AF8355E00EC53A8B0C864167 /* RotatingCardBrandsView.swift */,
3556971CA13C767092BE7A34 /* ShadowedRoundedRectangleView.swift */,
6BB97FB5D5730FE4CAB9298D /* SheetNavigationBar.swift */,
Expand Down Expand Up @@ -1668,6 +1676,7 @@
C830FEC205E7162FF4D414BE /* PaymentMethodMessagingViewFunctionalTest.swift */,
5BA7BFC43DB3EFD38A460EB9 /* PaymentMethodMessagingViewSnapshotTests.swift */,
619AF0842BF56C5E00D1C981 /* PaymentMethodRowButtonSnapshotTests.swift */,
CB225E952CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift */,
8A0B7F6E25D93C0C0ACE3B3D /* PaymentSheet+APITest.swift */,
31316BD62CEC00EF0000016F /* PaymentSheet+APIMockTest.swift */,
82C21D5722BDEB8BAA71F69F /* PaymentSheet+DashboardConfirmParamsTest.swift */,
Expand Down Expand Up @@ -1977,6 +1986,7 @@
31CC9B812CB5F69600E84A38 /* LinkInstantDebitMandateViewSnapshotTests.swift in Sources */,
31CC9B822CB5F69600E84A38 /* LinkURLGeneratorTests.swift in Sources */,
31CC9B852CB5F69600E84A38 /* LinkBadgeViewSnapshotTest.swift in Sources */,
CB225E962CEF80DC00054262 /* PaymentMethodTypeCollectionViewCellSnapshotTests.swift in Sources */,
B65FE7092BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift in Sources */,
37F750E1C99D6257E845A66E /* BacsDDMandateViewSnapshotTests.swift in Sources */,
694A3B36AC19FC1F87EF0CB1 /* CustomerSheetPaymentMethodAvailabilityTests.swift in Sources */,
Expand Down Expand Up @@ -2075,6 +2085,7 @@
F70BCDEECB5863244085F12F /* BoolReference.swift in Sources */,
8B1D7A7CE7D50382E9FA77E3 /* Images.swift in Sources */,
D3CC2489468E3288FD34C160 /* IntentStatusPoller.swift in Sources */,
CB46EF492CED1A2E00E9A7F2 /* PaymentMethodIncentive.swift in Sources */,
96C307CDEE7028B12D9CB69B /* PaymentSheetLinkAccount.swift in Sources */,
623C2D9F87929D6DA9C09E23 /* STPCameraView.swift in Sources */,
599337DB99E9E7017EF47BCE /* STPCardScanner.swift in Sources */,
Expand Down Expand Up @@ -2264,6 +2275,7 @@
AF0D609C28A8B0ECD11FD539 /* UpdatePaymentMethodViewController.swift in Sources */,
D203D701AF400680AF0F82F8 /* AUBECSMandate.swift in Sources */,
D792BA37B04E5A3AD30E37CF /* AffirmCopyLabel.swift in Sources */,
CB46EF4B2CED1BDA00E9A7F2 /* PromoBadgeView.swift in Sources */,
9BFC22175CF85F58B8B8792A /* AfterpayPriceBreakdownView.swift in Sources */,
A3B2F2BA2CF134B500C0E88C /* SavedPaymentMethodFormFactory+SEPADebit.swift in Sources */,
EA712D67C03385B9AD80288C /* Appearance+FontScaling.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ class CustomerAddPaymentMethodViewController: UIViewController {
}()
private lazy var paymentMethodTypesView: PaymentMethodTypeCollectionView = {
let view = PaymentMethodTypeCollectionView(
paymentMethodTypes: paymentMethodTypes, appearance: configuration.appearance, delegate: self)
paymentMethodTypes: paymentMethodTypes,
appearance: configuration.appearance,
incentive: nil,
delegate: self
)
return view
}()
private lazy var paymentMethodDetailsContainerView: DynamicHeightContainerView = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ class EmbeddedPaymentMethodsView: UIView {
let rowButton = RowButton.makeForPaymentMethodType(
paymentMethodType: paymentMethodType,
subtitle: VerticalPaymentMethodListViewController.subtitleText(for: paymentMethodType),
promoText: nil, // TODO(tillh-stripe) Pass promo text along
savedPaymentMethodType: savedPaymentMethod?.type,
appearance: rowButtonAppearance,
shouldAnimateOnPress: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ class AddPaymentMethodViewController: UIViewController {
private var paymentMethodFormElement: PaymentMethodElement {
paymentMethodFormViewController.form
}

private var incentive: PaymentMethodIncentive? {
elementsSession.linkSettings?.linkConsumerIncentive.flatMap { PaymentMethodIncentive(from: $0) }
}

// MARK: - Views
private lazy var paymentMethodFormViewController: PaymentMethodFormViewController = {
Expand All @@ -72,6 +76,7 @@ class AddPaymentMethodViewController: UIViewController {
paymentMethodTypes: paymentMethodTypes,
initialPaymentMethodType: previousCustomerInput?.paymentMethodType,
appearance: configuration.appearance,
incentive: incentive,
delegate: self
)
return view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ class PaymentMethodTypeCollectionView: UICollectionView {
let paymentMethodTypes: [PaymentSheet.PaymentMethodType]
let appearance: PaymentSheet.Appearance
weak var _delegate: PaymentMethodTypeCollectionViewDelegate?

private var incentive: PaymentMethodIncentive?

init(
paymentMethodTypes: [PaymentSheet.PaymentMethodType],
initialPaymentMethodType: PaymentSheet.PaymentMethodType? = nil,
appearance: PaymentSheet.Appearance,
incentive: PaymentMethodIncentive?,
delegate: PaymentMethodTypeCollectionViewDelegate
) {
stpAssert(!paymentMethodTypes.isEmpty, "At least one payment method type must be provided.")

self.paymentMethodTypes = paymentMethodTypes
self.incentive = incentive
self._delegate = delegate
let selectedItemIndex: Int = {
if let initialPaymentMethodType = initialPaymentMethodType {
Expand Down Expand Up @@ -113,7 +117,10 @@ extension PaymentMethodTypeCollectionView: UICollectionViewDataSource, UICollect
stpAssertionFailure()
return UICollectionViewCell()
}
let paymentMethodType = paymentMethodTypes[indexPath.item]
cell.paymentMethodType = paymentMethodType
cell.paymentMethodType = paymentMethodTypes[indexPath.item]
cell.promoBadgeText = incentive?.takeIfAppliesTo(paymentMethodType)?.displayText
cell.appearance = appearance
return cell
}
Expand Down Expand Up @@ -160,6 +167,12 @@ extension PaymentMethodTypeCollectionView {
update()
}
}

var promoBadgeText: String? = nil {
didSet {
update()
}
}

var appearance: PaymentSheet.Appearance = PaymentSheet.Appearance.default {
didSet {
Expand All @@ -182,6 +195,10 @@ extension PaymentMethodTypeCollectionView {
paymentMethodLogo.contentMode = .scaleAspectFit
return paymentMethodLogo
}()
private lazy var promoBadge: PromoBadgeView = {
let font = appearance.scaledFont(for: appearance.font.base.medium, style: .footnote, maximumPointSize: 20)
return PromoBadgeView(appearance: appearance, tinyMode: true)
}()
private lazy var shadowRoundedRectangle: ShadowedRoundedRectangle = {
return ShadowedRoundedRectangle(appearance: appearance)
}()
Expand Down Expand Up @@ -210,7 +227,7 @@ extension PaymentMethodTypeCollectionView {
override init(frame: CGRect) {
super.init(frame: frame)

[paymentMethodLogo, label].forEach {
[paymentMethodLogo, label, promoBadge].forEach {
shadowRoundedRectangle.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
Expand All @@ -233,6 +250,10 @@ extension PaymentMethodTypeCollectionView {
equalTo: shadowRoundedRectangle.bottomAnchor, constant: -8),
label.leadingAnchor.constraint(equalTo: paymentMethodLogo.leadingAnchor),
label.trailingAnchor.constraint(equalTo: shadowRoundedRectangle.trailingAnchor, constant: -12), // should be -const of paymentMethodLogo leftAnchor

promoBadge.topAnchor.constraint(equalTo: paymentMethodLogo.topAnchor),
promoBadge.trailingAnchor.constraint(equalTo: shadowRoundedRectangle.trailingAnchor, constant: -12),
promoBadge.bottomAnchor.constraint(equalTo: paymentMethodLogo.bottomAnchor),
])

contentView.layer.cornerRadius = appearance.cornerRadius
Expand Down Expand Up @@ -274,9 +295,11 @@ extension PaymentMethodTypeCollectionView {
case .shouldDisableUserInteraction:
self.label.alpha = 0.6
self.paymentMethodLogo.alpha = 0.6
self.promoBadge.alpha = 0.6
case .shouldEnableUserInteraction:
self.label.alpha = 1
self.paymentMethodLogo.alpha = 1
self.promoBadge.alpha = 1
default:
break
}
Expand Down Expand Up @@ -308,6 +331,11 @@ extension PaymentMethodTypeCollectionView {
if paymentMethodTypeOfCurrentImage != self.paymentMethodType || image.size != CGSize(width: 1, height: 1) {
updateImage(image)
}

promoBadge.isHidden = promoBadgeText == nil
if let promoBadgeText {
promoBadge.setText(promoBadgeText)
}

shadowRoundedRectangle.isSelected = isSelected
// Set text color
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// PaymentMethodIncentive.swift
// StripePaymentSheet
//
// Created by Till Hellmund on 11/19/24.
//

import Foundation
@_spi(STP) import StripePayments

struct PaymentMethodIncentive {

private let identifier: String
let displayText: String

func takeIfAppliesTo(_ paymentMethodType: PaymentSheet.PaymentMethodType) -> PaymentMethodIncentive? {
switch paymentMethodType {
case .stripe, .external:
return nil
case .instantDebits, .linkCardBrand:
return identifier == "link_instant_debits" ? self : nil
}
}
}

extension PaymentMethodIncentive {

init?(from incentive: LinkConsumerIncentive) {
guard let displayText = incentive.incentiveDisplayText else {
return nil
}

self.identifier = incentive.incentiveParams.paymentMethod
self.displayText = displayText
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class RowButton: UIView {
let label: UILabel
let sublabel: UILabel?
let rightAccessoryView: UIView?
let promoBadge: PromoBadgeView?
let shouldAnimateOnPress: Bool
let appearance: PaymentSheet.Appearance
typealias DidTapClosure = (RowButton) -> Void
Expand All @@ -58,7 +59,17 @@ class RowButton: UIView {
}
var heightConstraint: NSLayoutConstraint?

init(appearance: PaymentSheet.Appearance, imageView: UIImageView, text: String, subtext: String? = nil, rightAccessoryView: UIView? = nil, shouldAnimateOnPress: Bool = false, isEmbedded: Bool = false, didTap: @escaping DidTapClosure) {
init(
appearance: PaymentSheet.Appearance,
imageView: UIImageView,
text: String,
subtext: String? = nil,
promoText: String? = nil,
rightAccessoryView: UIView? = nil,
shouldAnimateOnPress: Bool = false,
isEmbedded: Bool = false,
didTap: @escaping DidTapClosure
) {
self.appearance = appearance
self.shouldAnimateOnPress = true
self.didTap = didTap
Expand All @@ -79,6 +90,15 @@ class RowButton: UIView {
} else {
self.sublabel = nil
}
if let promoText {
self.promoBadge = PromoBadgeView(
appearance: appearance,
tinyMode: false,
text: promoText
)
} else {
self.promoBadge = nil
}
super.init(frame: .zero)

// Label and sublabel
Expand Down Expand Up @@ -123,6 +143,28 @@ class RowButton: UIView {
checkmarkImageView.heightAnchor.constraint(equalToConstant: 16),
])
}

if let promoBadge {
let promoBadgePadding: CGFloat = {
guard isEmbedded else {
return -12
}

switch appearance.embeddedPaymentElement.row.style {
case .flatWithRadio, .flatWithCheckmark:
return 0
case .floatingButton:
return -12
}
}()
promoBadge.translatesAutoresizingMaskIntoConstraints = false
addSubview(promoBadge)
NSLayoutConstraint.activate([
promoBadge.topAnchor.constraint(equalTo: topAnchor),
promoBadge.bottomAnchor.constraint(equalTo: bottomAnchor),
promoBadge.trailingAnchor.constraint(equalTo: rightAccessoryView?.leadingAnchor ?? trailingAnchor, constant: promoBadgePadding),
])
}

for view in [radioButton, imageView, labelsStackView].compactMap({ $0 }) {
view.translatesAutoresizingMaskIntoConstraints = false
Expand Down Expand Up @@ -172,7 +214,7 @@ class RowButton: UIView {
radioButton?.widthAnchor.constraint(equalToConstant: 18),

labelsStackView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 12),
labelsStackView.trailingAnchor.constraint(equalTo: labelTrailingConstant, constant: -12),
labelsStackView.trailingAnchor.constraint(equalTo: promoBadge?.leadingAnchor ?? labelTrailingConstant, constant: -12),
labelsStackView.centerYAnchor.constraint(equalTo: centerYAnchor),
labelsStackView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor, constant: appearance.embeddedPaymentElement.row.additionalInsets),
labelsStackView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -appearance.embeddedPaymentElement.row.additionalInsets),
Expand Down Expand Up @@ -271,10 +313,12 @@ extension RowButton: EventHandler {
label.alpha = 1
sublabel?.alpha = 1
imageView.alpha = 1
promoBadge?.alpha = 1
case .shouldDisableUserInteraction:
label.alpha = 0.5
sublabel?.alpha = 0.5
imageView.alpha = 0.5
promoBadge?.alpha = 0.5
default:
break
}
Expand Down Expand Up @@ -330,7 +374,17 @@ extension RowButton {
return label
}

static func makeForPaymentMethodType(paymentMethodType: PaymentSheet.PaymentMethodType, subtitle: String? = nil, savedPaymentMethodType: STPPaymentMethodType?, appearance: PaymentSheet.Appearance, shouldAnimateOnPress: Bool, isEmbedded: Bool = false, didTap: @escaping DidTapClosure) -> RowButton {
static func makeForPaymentMethodType(
paymentMethodType: PaymentSheet.PaymentMethodType,
subtitle: String? = nil,
promoText: String? = nil,
rightAccessoryView: UIView? = nil,
savedPaymentMethodType: STPPaymentMethodType?,
appearance: PaymentSheet.Appearance,
shouldAnimateOnPress: Bool,
isEmbedded: Bool = false,
didTap: @escaping DidTapClosure
) -> RowButton {
let imageView = PaymentMethodTypeImageView(paymentMethodType: paymentMethodType, backgroundColor: appearance.colors.componentBackground)
imageView.contentMode = .scaleAspectFit
// Special case "New card" vs "Card" title
Expand All @@ -340,7 +394,7 @@ extension RowButton {
}
return paymentMethodType.displayName
}()
return RowButton(appearance: appearance, imageView: imageView, text: text, subtext: subtitle, shouldAnimateOnPress: shouldAnimateOnPress, isEmbedded: isEmbedded, didTap: didTap)
return RowButton(appearance: appearance, imageView: imageView, text: text, subtext: subtitle, promoText: promoText, rightAccessoryView: rightAccessoryView, shouldAnimateOnPress: shouldAnimateOnPress, isEmbedded: isEmbedded, didTap: didTap)
}

static func makeForApplePay(appearance: PaymentSheet.Appearance, isEmbedded: Bool = false, didTap: @escaping DidTapClosure) -> RowButton {
Expand Down
Loading
Loading