Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// Copyright © 2022 Stripe, Inc. All rights reserved.
//

@_spi(STP) import StripeCore
import UIKit

extension NSMutableAttributedString {
Expand All @@ -15,97 +14,4 @@ extension NSMutableAttributedString {
replaceCharacters(in: NSRange(range, in: string), with: replacement)
}
}

/// Generates an attributed for use in BNPL info context. Adds line spacing, an info icon at the end, and optionally substitutes a BNPL logo in for a placeholder in the template.
/// - Parameters:
/// - template: The promotional text to be displayed, including a placeholder if needed (e.g. "Buy now or pay later with {partner}")
/// - substitution: An optional tuple containing the placeholder text from the template to be replaced and the partner logo image to replace it with.
static func bnplPromoString(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Separated this back out into two separate implementations — one for PMME and one for the afterpay header. They do pretty different things at this point, and the afterpay one is about to be removed and replaced with PMME anyways

This comment was marked as spam.

font: UIFont,
textColor: UIColor,
infoIconColor: UIColor,
template: String,
substitution: (placeholder: String, bnplLogo: UIImage)?
) -> NSMutableAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 2
let stringAttributes = [
NSAttributedString.Key.font: font,
.foregroundColor: textColor,
.paragraphStyle: paragraphStyle,
]

let resultingString = NSMutableAttributedString()
resultingString.append(NSAttributedString(string: ""))

// Replace placeholder with BNPL image if needed
if let (partnerPlaceholder, bnplLogoImage) = substitution {
guard let img = template.range(of: partnerPlaceholder) else {
return resultingString
}

var imgAppended = false

// Go through string, replacing the placeholder with the BNPL logo
for (indexOffset, currCharacter) in template.enumerated() {
let currIndex = template.index(template.startIndex, offsetBy: indexOffset)
if img.contains(currIndex) {
if imgAppended {
continue
}
imgAppended = true

// Add BNPL logo. Use additioanl scale of 2x
let bnplLogo = Self.attributedStringOfImageWithoutLink(uiImage: bnplLogoImage, font: font, additionalScale: 2.0)
resultingString.append(bnplLogo)
} else {
resultingString.append(NSAttributedString(string: String(currCharacter),
attributes: stringAttributes))
}
}
} else {
// Otherwise just fill in the whole template
resultingString.append(NSAttributedString(string: template, attributes: stringAttributes))
}

// Add info icon. Use additional scale of 1.5x
let symbolConfig = UIImage.SymbolConfiguration(pointSize: font.pointSize)
if let infoIconImage = UIImage(systemName: "info.circle", withConfiguration: symbolConfig)?
.withTintColor(infoIconColor, renderingMode: .alwaysTemplate) {
let infoIcon = Self.attributedStringOfImageWithoutLink(uiImage: infoIconImage, font: font, additionalScale: 1.5)
resultingString.append(NSAttributedString(string: "\u{00A0}", attributes: stringAttributes))
resultingString.append(infoIcon)
} else {
stpAssertionFailure("Failed to load system image info.circle")
}

return resultingString
}

// Returns an attributed string containing only a text attachment for the given image.
// The image is scaled so that its height matches the `.capHeight` of the font, and it is vertically centered.
// An additionalScale can be provided to make the image taller or shorter than the text.
private static func attributedStringOfImageWithoutLink(
uiImage: UIImage,
font: UIFont,
additionalScale: CGFloat
) -> NSAttributedString {
let imageAttachment = NSTextAttachment()
imageAttachment.bounds = boundsOfImage(font: font, uiImage: uiImage, additionalScale: additionalScale)
imageAttachment.image = uiImage
return NSAttributedString(attachment: imageAttachment)
}

// Originally based on https://stackoverflow.com/questions/26105803/center-nstextattachment-image-next-to-single-line-uilabel
private static func boundsOfImage(font: UIFont, uiImage: UIImage, additionalScale: CGFloat) -> CGRect {
let scaledSize = uiImage.sizeMatchingFont(font, additionalScale: additionalScale)
// Calculate the difference in height between the scaled image and the font
let heightDifference = font.capHeight - scaledSize.height
// To vertically center the image, we want to vertically offset it by half of the height difference between it and the font
let verticalOffset = heightDifference.rounded() / 2
return CGRect(
origin: .init(x: 0, y: verticalOffset),
size: scaledSize
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//

This comment was marked as spam.

// PMME+AttributedString.swift
// StripePaymentSheet
//

@_spi(STP) import StripeCore
import UIKit

extension NSMutableAttributedString {
/// Generates an attributed string for PMME promotional text.
/// Adds line spacing, optionally substitutes a BNPL logo for a placeholder, and appends info message text with underline.
/// - Parameters:
/// - template: The promotional text to be displayed, including a placeholder if needed (e.g. "Buy now or pay later with {partner}")
/// - substitution: An optional tuple containing the placeholder text from the template to be replaced and the partner logo image to replace it with.
/// - infoMessage: The text to display at the end with underline styling.
static func pmmePromoString(
font: UIFont,
textColor: UIColor,
template: String,
substitution: (placeholder: String, bnplLogo: UIImage)?,
infoMessage: String
) -> NSMutableAttributedString {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineSpacing = 2
let stringAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: textColor,
.paragraphStyle: paragraphStyle,
]

let infoMessageAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: textColor,
.paragraphStyle: paragraphStyle,
.underlineStyle: NSUnderlineStyle.single.rawValue,
]

let resultingString = NSMutableAttributedString()

// Replace placeholder with BNPL image if needed
if let (partnerPlaceholder, bnplLogoImage) = substitution {
guard let img = template.range(of: partnerPlaceholder) else {
return resultingString
}

var imgAppended = false

// Go through string, replacing the placeholder with the BNPL logo
for (indexOffset, currCharacter) in template.enumerated() {
let currIndex = template.index(template.startIndex, offsetBy: indexOffset)
if img.contains(currIndex) {
if imgAppended {
continue
}
imgAppended = true

// Add BNPL logo. Use additional scale of 2x
let bnplLogo = attributedStringOfImageWithoutLink(uiImage: bnplLogoImage, font: font, additionalScale: 2.0)
resultingString.append(bnplLogo)
} else {
resultingString.append(NSAttributedString(string: String(currCharacter),
attributes: stringAttributes))
}
}
} else {
// Otherwise just fill in the whole template
resultingString.append(NSAttributedString(string: template, attributes: stringAttributes))
}

// Add info message text with underline
resultingString.append(NSAttributedString(string: " ", attributes: stringAttributes))
resultingString.append(NSAttributedString(string: infoMessage, attributes: infoMessageAttributes))

return resultingString
}

// Returns an attributed string containing only a text attachment for the given image.
// The image is scaled so that its height matches the `.capHeight` of the font, and it is vertically centered.
// An additionalScale can be provided to make the image taller or shorter than the text.
private static func attributedStringOfImageWithoutLink(
uiImage: UIImage,
font: UIFont,
additionalScale: CGFloat
) -> NSAttributedString {
let imageAttachment = NSTextAttachment()
imageAttachment.bounds = boundsOfImage(font: font, uiImage: uiImage, additionalScale: additionalScale)
imageAttachment.image = uiImage
return NSAttributedString(attachment: imageAttachment)
}

// Originally based on https://stackoverflow.com/questions/26105803/center-nstextattachment-image-next-to-single-line-uilabel
private static func boundsOfImage(font: UIFont, uiImage: UIImage, additionalScale: CGFloat) -> CGRect {
let scaledSize = uiImage.sizeMatchingFont(font, additionalScale: additionalScale)

This comment was marked as spam.

// Calculate the difference in height between the scaled image and the font
let heightDifference = font.capHeight - scaledSize.height
// To vertically center the image, we want to vertically offset it by half of the height difference between it and the font
let verticalOffset = heightDifference.rounded() / 2
return CGRect(
origin: .init(x: 0, y: verticalOffset),
size: scaledSize
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ extension PaymentMethodMessagingElement {
let legalDisclosure = paymentPlan.content.legalDisclosure?.message

This comment was marked as spam.


// unexpected / error cases
guard let infoUrl = paymentPlan.content.learnMore?.url else {
guard let infoData = paymentPlan.content.learnMore else {
Self.assertAndLogMissingField("learn_more", apiClient: configuration.apiClient)
return nil
}
let infoMessage = infoData.message
guard let infoUrl = infoData.url else {
Self.assertAndLogMissingField("info_url", apiClient: configuration.apiClient)
return nil
}
Expand All @@ -42,6 +47,7 @@ extension PaymentMethodMessagingElement {
if let topLevelPromotion = apiResponse.content.promotion?.message {
self.init(
mode: .multiPartner(logos: []),
infoMessage: infoMessage,
infoUrl: infoUrl,
legalDisclosure: legalDisclosure,
promotion: topLevelPromotion,
Expand All @@ -58,6 +64,7 @@ extension PaymentMethodMessagingElement {
// success
self.init(
mode: .singlePartner(logo: logo),
infoMessage: infoMessage,
infoUrl: infoUrl,
legalDisclosure: legalDisclosure,
promotion: inlinePromo,
Expand All @@ -77,7 +84,12 @@ extension PaymentMethodMessagingElement {
let legalDisclosure = apiResponse.content.legalDisclosure?.message

// unexpected / error case
guard let infoUrl = apiResponse.content.learnMore?.url else {
guard let infoData = apiResponse.content.learnMore else {
Self.assertAndLogMissingField("learn_more", apiClient: configuration.apiClient)
return nil
}
let infoMessage = infoData.message
guard let infoUrl = infoData.url else {
Self.assertAndLogMissingField("info_url", apiClient: configuration.apiClient)
return nil
}
Expand All @@ -93,6 +105,7 @@ extension PaymentMethodMessagingElement {
// success
self.init(
mode: .multiPartner(logos: logos),
infoMessage: infoMessage,
infoUrl: infoUrl,
legalDisclosure: legalDisclosure,
promotion: promo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class PMMEMultiPartnerView: UIView {

private let logoSets: [PaymentMethodMessagingElement.LogoSet]
private let promotion: String
private let infoMessage: String
private let appearance: PaymentMethodMessagingElement.Appearance

private var logoViews = [UIImageView]()
Expand Down Expand Up @@ -43,10 +44,12 @@ class PMMEMultiPartnerView: UIView {
init(
logoSets: [PaymentMethodMessagingElement.LogoSet],
promotion: String,
infoMessage: String,
appearance: PaymentMethodMessagingElement.Appearance
) {
self.logoSets = logoSets
self.promotion = promotion
self.infoMessage = infoMessage
self.appearance = appearance
super.init(frame: .zero)

Expand Down Expand Up @@ -108,13 +111,14 @@ class PMMEMultiPartnerView: UIView {
}
}

private func getPromotionAttributedString() -> NSMutableAttributedString? {
return NSMutableAttributedString.bnplPromoString(
private func getPromotionAttributedString() -> NSMutableAttributedString {
let promotionWithPeriod = promotion.hasSuffix(".") ? promotion : promotion + "."
return NSMutableAttributedString.pmmePromoString(
font: appearance.scaledFont,
textColor: appearance.textColor,
infoIconColor: appearance.infoIconColor ?? appearance.textColor,
template: promotion,
substitution: nil
template: promotionWithPeriod,
substitution: nil,
infoMessage: infoMessage
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PMMESinglePartnerView: UIView {

private let logoSet: PaymentMethodMessagingElement.LogoSet
private let promotion: String
private let infoMessage: String
private let appearance: PaymentMethodMessagingElement.Appearance

private let promotionLabel = UILabel()
Expand All @@ -25,10 +26,12 @@ class PMMESinglePartnerView: UIView {
init(
logoSet: PaymentMethodMessagingElement.LogoSet,
promotion: String,
infoMessage: String,
appearance: PaymentMethodMessagingElement.Appearance
) {
self.logoSet = logoSet
self.promotion = promotion
self.infoMessage = infoMessage
self.appearance = appearance
super.init(frame: .zero)

Expand All @@ -54,12 +57,12 @@ class PMMESinglePartnerView: UIView {
}

func getPromotionAttributedString() -> NSMutableAttributedString {
NSMutableAttributedString.bnplPromoString(
NSMutableAttributedString.pmmePromoString(
font: appearance.scaledFont,
textColor: appearance.textColor,
infoIconColor: appearance.infoIconColor ?? appearance.textColor,
template: promotion,
substitution: ("{partner}", traitCollection.isDarkMode ? logoSet.dark : logoSet.light)
substitution: ("{partner}", traitCollection.isDarkMode ? logoSet.dark : logoSet.light),
infoMessage: infoMessage
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ class PMMEUIView: UIView {
// choose which style to initialize
switch viewData.mode {
case .singlePartner(let logo):
let view = PMMESinglePartnerView(logoSet: logo, promotion: viewData.promotion, appearance: appearance)
let view = PMMESinglePartnerView(logoSet: logo, promotion: viewData.promotion, infoMessage: viewData.infoMessage, appearance: appearance)
stackView.addArrangedSubview(view)
accessibilityLabel = view.customAccessibilityLabel
case .multiPartner(let logos):
let view = PMMEMultiPartnerView(logoSets: logos, promotion: viewData.promotion, appearance: appearance)
let view = PMMEMultiPartnerView(logoSets: logos, promotion: viewData.promotion, infoMessage: viewData.infoMessage, appearance: appearance)
stackView.addArrangedSubview(view)
accessibilityLabel = view.customAccessibilityLabel
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,14 @@ extension PaymentMethodMessagingElement {

/// The element's view data for the SwiftUI view.
public var viewData: ViewData {
.init(mode: mode, infoUrl: infoUrl, legalDisclosure: legalDisclosure, promotion: promotion, appearance: appearance, analyticsHelper: analyticsHelper)
.init(mode: mode, infoMessage: infoMessage, infoUrl: infoUrl, legalDisclosure: legalDisclosure, promotion: promotion, appearance: appearance, analyticsHelper: analyticsHelper)
}

/// Displayable data of an initialized Payment Method Messaging Element.
/// For use by PaymentMethodMessagingElement.View.
public struct ViewData {
let mode: Mode
let infoMessage: String
let infoUrl: URL
let legalDisclosure: String?
let promotion: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,16 @@ public class PaymentMethodMessagingElement {
// MARK: - Internal

let mode: Mode
let infoMessage: String
let infoUrl: URL
let legalDisclosure: String?
let promotion: String
let appearance: Appearance
let analyticsHelper: PMMEAnalyticsHelper

init(mode: Mode, infoUrl: URL, legalDisclosure: String?, promotion: String, appearance: PaymentMethodMessagingElement.Appearance, analyticsHelper: PMMEAnalyticsHelper) {
init(mode: Mode, infoMessage: String, infoUrl: URL, legalDisclosure: String?, promotion: String, appearance: PaymentMethodMessagingElement.Appearance, analyticsHelper: PMMEAnalyticsHelper) {
self.mode = mode
self.infoMessage = infoMessage
self.infoUrl = infoUrl
self.legalDisclosure = legalDisclosure
self.promotion = promotion
Expand Down
Loading
Loading