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

Embedded update pt 2 - update view #4150

Merged
merged 3 commits into from
Oct 21, 2024
Merged
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
12 changes: 10 additions & 2 deletions StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
B63B2CF32BFBEE7B003810F3 /* VerticalPaymentMethodListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B2CF22BFBEE7B003810F3 /* VerticalPaymentMethodListViewController.swift */; };
B63B2CF52BFBEEAD003810F3 /* PaymentMethodFormViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B2CF42BFBEEAD003810F3 /* PaymentMethodFormViewController.swift */; };
B63B2CF72BFC116A003810F3 /* PaymentSheetVerticalViewControllerSnapshotTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63B2CF62BFC116A003810F3 /* PaymentSheetVerticalViewControllerSnapshotTest.swift */; };
B63DC6782CC054410011C27E /* EmbeddedPaymentElementContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63DC6772CC0543D0011C27E /* EmbeddedPaymentElementContainerView.swift */; };
B63DC67A2CC06AD10011C27E /* EmbeddedPaymentElementSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B63DC6792CC06AC80011C27E /* EmbeddedPaymentElementSnapshotTests.swift */; };
B64FEF122C0FAC1E00F7CA26 /* PaymentSheetVerticalViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64FEF112C0FAC1E00F7CA26 /* PaymentSheetVerticalViewControllerTest.swift */; };
B65B42972C013DED00EC565D /* PaymentMethodFormViewControllerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65B42962C013DED00EC565D /* PaymentMethodFormViewControllerTest.swift */; };
B65FE7092BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65FE7082BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift */; };
Expand Down Expand Up @@ -599,6 +601,8 @@
B63B2CF22BFBEE7B003810F3 /* VerticalPaymentMethodListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalPaymentMethodListViewController.swift; sourceTree = "<group>"; };
B63B2CF42BFBEEAD003810F3 /* PaymentMethodFormViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentMethodFormViewController.swift; sourceTree = "<group>"; };
B63B2CF62BFC116A003810F3 /* PaymentSheetVerticalViewControllerSnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetVerticalViewControllerSnapshotTest.swift; sourceTree = "<group>"; };
B63DC6772CC0543D0011C27E /* EmbeddedPaymentElementContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentElementContainerView.swift; sourceTree = "<group>"; };
B63DC6792CC06AC80011C27E /* EmbeddedPaymentElementSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedPaymentElementSnapshotTests.swift; sourceTree = "<group>"; };
B64FEF112C0FAC1E00F7CA26 /* PaymentSheetVerticalViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetVerticalViewControllerTest.swift; sourceTree = "<group>"; };
B65B42962C013DED00EC565D /* PaymentMethodFormViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodFormViewControllerTest.swift; sourceTree = "<group>"; };
B65FE7082BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalPaymentMethodListViewControllerSnapshotTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -930,15 +934,16 @@
6180A5C52C82434A009D1536 /* Embedded */ = {
isa = PBXGroup;
children = (
6117D70F2CB0654B005C4EC1 /* EmbeddedMandateContainerView.swift */,
B615E8702CA4CBEE007D684C /* EmbeddedPaymentElement.swift */,
B6CACC9D2CB8B8E800682ECE /* EmbeddedPaymentElement+Internal.swift */,
B615E8722CA4CC04007D684C /* EmbeddedPaymentElementConfiguration.swift */,
B63DC6772CC0543D0011C27E /* EmbeddedPaymentElementContainerView.swift */,
B615E8742CA4CC38007D684C /* EmbeddedPaymentElementDelegate.swift */,
6180A5C02C8222A9009D1536 /* EmbeddedPaymentMethodsView.swift */,
6117D7112CB065E7005C4EC1 /* MandateTextProvider.swift */,
6180A5C62C824377009D1536 /* RadioButton.swift */,
6180A5CA2C8249D2009D1536 /* UIStackView+Separator.swift */,
6117D7112CB065E7005C4EC1 /* MandateTextProvider.swift */,
6117D70F2CB0654B005C4EC1 /* EmbeddedMandateContainerView.swift */,
);
path = Embedded;
sourceTree = "<group>";
Expand Down Expand Up @@ -1430,6 +1435,7 @@
31699A822BE183D40048677F /* DownloadManagerTest.swift */,
73FB30705EC36BD0868904A2 /* Elements+TestHelpers.swift */,
B6CACC9F2CBD9A3300682ECE /* EmbeddedPaymentElementTest.swift */,
B63DC6792CC06AC80011C27E /* EmbeddedPaymentElementSnapshotTests.swift */,
61FB6BCC2C8901B200F8E074 /* EmbeddedPaymentMethodsViewSnapshotTests.swift */,
614068E12CB0BF10003D2F12 /* EmbeddedPaymentMethodsViewTests.swift */,
64C8F350CDB5A29F62E86592 /* FlowControllerStateTests.swift */,
Expand Down Expand Up @@ -1732,6 +1738,7 @@
10A336F0F2331F22F1A0AC1B /* LinkStubs.swift in Sources */,
61C87E1E2CB81FAD001B7DA9 /* CardBrandFilterTests.swift in Sources */,
316B33122B5F171C0008D2E5 /* UserDefaults+StripePaymentSheetTest.swift in Sources */,
B63DC67A2CC06AD10011C27E /* EmbeddedPaymentElementSnapshotTests.swift in Sources */,
D77514C28C9A031908E99CA1 /* PaymentMethodMessagingViewFunctionalTest.swift in Sources */,
D14478CFCABDF7455DA7472A /* PaymentMethodMessagingViewSnapshotTests.swift in Sources */,
3D3607748436E625FF6CF921 /* PaymentSheet+APITest.swift in Sources */,
Expand Down Expand Up @@ -1900,6 +1907,7 @@
88BA38BE8949815F4DB79509 /* PayWithLinkController.swift in Sources */,
6B28A6B62BE9494500B47DBF /* CustomerSheetDataSource.swift in Sources */,
8C91277A8FEFD0B914CC6564 /* PaymentSheet-LinkConfirmOption.swift in Sources */,
B63DC6782CC054410011C27E /* EmbeddedPaymentElementContainerView.swift in Sources */,
573E3DB554058AC1E34E34B6 /* AddPaymentMethodViewController.swift in Sources */,
8D4951AE0D793D01528F352D /* PaymentMethodTypeCollectionView.swift in Sources */,
2CE83364A23B4E3BAFD447CA /* WalletHeaderView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ public final class EmbeddedPaymentElement {

/// A view that displays payment methods. It can present a sheet to collect more details or display saved payment methods.
public var view: UIView {
// TODO: Make this a _container view_ so that we can swap out the inner `embeddedPaymentMethodsView` when `update` is called.
return embeddedPaymentMethodsView
return containerView
}

/// A view controller to present on.
Expand Down Expand Up @@ -145,6 +144,7 @@ public final class EmbeddedPaymentElement {
// At this point, we're the latest update - update self properties and inform our delegate.
self.loadResult = loadResult
self.embeddedPaymentMethodsView = embeddedPaymentMethodsView
self.containerView.updateEmbeddedPaymentMethodsView(embeddedPaymentMethodsView)
if paymentOption != embeddedPaymentMethodsView.displayData {
self.delegate?.embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: self)
}
Expand All @@ -164,6 +164,7 @@ public final class EmbeddedPaymentElement {

// MARK: - Internal

internal private(set) var containerView: EmbeddedPaymentElementContainerView
internal private(set) var embeddedPaymentMethodsView: EmbeddedPaymentMethodsView
internal private(set) var loadResult: PaymentSheetLoader.LoadResult
internal private(set) var currentUpdateTask: Task<UpdateResult, Never>?
Expand All @@ -178,6 +179,14 @@ public final class EmbeddedPaymentElement {
configuration: configuration,
loadResult: loadResult
)
self.containerView = EmbeddedPaymentElementContainerView(
embeddedPaymentMethodsView: embeddedPaymentMethodsView
)

self.containerView.updateSuperviewHeight = { [weak self] in
porter-stripe marked this conversation as resolved.
Show resolved Hide resolved
guard let self else { return }
self.delegate?.embeddedPaymentElementDidUpdateHeight(embeddedPaymentElement: self)
}
self.embeddedPaymentMethodsView.delegate = self
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// EmbeddedPaymentElementContainerView.swift
// StripePaymentSheet
//
// Created by Yuki Tokuhiro on 10/16/24.
//
import UIKit

/// The view that's vended to the merchant, containing the embedded view. We use this to be able to swap out the embedded view with an animation when `update` is called.
class EmbeddedPaymentElementContainerView: UIView {
var updateSuperviewHeight: () -> Void = {}
var view: UIView
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any reason these aren't private (excluding updateSuperviewHeight, although that feels like it should come in at init time and be optional)? Also wondering it it ever makes sense to use this class when view is not of type EmbeddedPaymentMethodsView. Should it just be EmbeddedPaymentMethodsView over UIView in this class?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

updateSuperviewHeight is a var for convenience; self isn't available when EmbeddedPaymentElement initializes this.

Will make them private & change the UIView type though!

var bottomAnchorConstraint: NSLayoutConstraint!

init(embeddedPaymentMethodsView: UIView) {
self.view = embeddedPaymentMethodsView
super.init(frame: .zero)
addInitialView(view)
}

required init?(coder: NSCoder) {
fatalError()
}

private func addInitialView(_ view: UIView) {
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
bottomAnchorConstraint = view.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor)
NSLayoutConstraint.activate([
view.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
view.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
view.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
bottomAnchorConstraint,
])
}

func updateEmbeddedPaymentMethodsView(_ embeddedPaymentMethodsView: UIView) {
guard frame.size != .zero else {
// A zero frame means we haven't been laid out yet. Simply replace the old view to avoid laying out before the view is ready and breaking constraints.
self.view.removeFromSuperview()
self.view = embeddedPaymentMethodsView
addInitialView(embeddedPaymentMethodsView)
return
}
let oldView = view
let oldViewHeight = frame.height
// Add the new view w/ 0 alpha
embeddedPaymentMethodsView.translatesAutoresizingMaskIntoConstraints = false
addSubview(embeddedPaymentMethodsView)
view = embeddedPaymentMethodsView
embeddedPaymentMethodsView.alpha = 0
NSLayoutConstraint.activate([
embeddedPaymentMethodsView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
embeddedPaymentMethodsView.leadingAnchor.constraint(equalTo: layoutMarginsGuide.leadingAnchor),
embeddedPaymentMethodsView.trailingAnchor.constraint(equalTo: layoutMarginsGuide.trailingAnchor),
// Omit the bottom anchor so that the height is still fixed to the old view height
])
// important that the view is already laid out before the animation block so that it doesn't animate from zero size.
layoutIfNeeded()

UIView.animate(withDuration: 0.2) {
// Re-pin bottom anchor to the new view, thus updating our height
self.bottomAnchorConstraint.isActive = false
self.bottomAnchorConstraint = embeddedPaymentMethodsView.bottomAnchor.constraint(equalTo: self.layoutMarginsGuide.bottomAnchor)
self.bottomAnchorConstraint.isActive = true
self.layoutIfNeeded()
// Fade old view out and new view in
oldView.alpha = 0
embeddedPaymentMethodsView.alpha = 1
if oldViewHeight != self.systemLayoutSizeFitting(.zero).height {
// Invoke EmbeddedPaymentElement delegate method so that height does not jump
self.updateSuperviewHeight()
}
} completion: { _ in
oldView.removeFromSuperview()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// EmbeddedPaymentElementSnapshotTests.swift
// StripePaymentSheet
//
// Created by Yuki Tokuhiro on 10/16/24.
//

import StripeCoreTestUtils
@_spi(STP) @testable import StripePayments
@_spi(EmbeddedPaymentElementPrivateBeta) @testable import StripePaymentSheet
@testable import StripePaymentsTestUtils
@_spi(STP) @testable import StripeUICore
import XCTest

class EmbeddedPaymentElementSnapshotTests: STPSnapshotTestCase, EmbeddedPaymentElementDelegate {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Are we planning to add more tests to this file? If so do we have a ticket or will that happen as a follow up?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep will add more tests with the next PRs (see bullets in summary)!

Copy link
Collaborator

Choose a reason for hiding this comment

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

ACK, saw bullets just didn't think they'd be snapshot tests? Haven't thought much about it though.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh, missed "in this file". I think we'll eventually add but I don't have any specific tests in mind for update.

var delegateDidUpdateHeightCalled: Bool = false
var delegateDidUpdatePaymentOptionCalled: Bool = false
func embeddedPaymentElementDidUpdateHeight(embeddedPaymentElement: StripePaymentSheet.EmbeddedPaymentElement) {
self.delegateDidUpdateHeightCalled = true
}

func embeddedPaymentElementDidUpdatePaymentOption(embeddedPaymentElement: StripePaymentSheet.EmbeddedPaymentElement) {
self.delegateDidUpdatePaymentOptionCalled = true
}

lazy var configuration: EmbeddedPaymentElement.Configuration = {
var config = EmbeddedPaymentElement.Configuration._testValue_MostPermissive(isApplePayEnabled: false)
config.apiClient = STPAPIClient(publishableKey: STPTestingDefaultPublishableKey)
return config
}()
let paymentIntentConfig = EmbeddedPaymentElement.IntentConfiguration(mode: .payment(amount: 1000, currency: "USD"), paymentMethodTypes: ["card"]) { _, _, _ in
// These tests don't confirm, so this is unused
}
let setupIntentConfig = EmbeddedPaymentElement.IntentConfiguration(mode: .setup(setupFutureUsage: .offSession), paymentMethodTypes: ["card", "us_bank_account"]) { _, _, _ in
// These tests don't confirm, so this is unused
}

func testUpdateFromCardToCardAndUSBankAccount() async throws {
// Given a EmbeddedPaymentElement instance...
let sut = try await EmbeddedPaymentElement.create(intentConfiguration: paymentIntentConfig, configuration: configuration)
sut.delegate = self
sut.view.autosizeHeight(width: 300)

let loadResult = await sut.update(intentConfiguration: setupIntentConfig)
XCTAssertEqual(loadResult, .succeeded)
sut.view.autosizeHeight(width: 300)

STPSnapshotVerifyView(sut.view) // Should show US Bank and card
XCTAssertTrue(delegateDidUpdateHeightCalled)
XCTAssertFalse(delegateDidUpdatePaymentOptionCalled)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class EmbeddedPaymentElementTest: STPNetworkStubbingTestCase {
XCTAssertEqual(updateResult2, .succeeded)
XCTAssertTrue(sut.loadResult.intent.isSettingUp)
}

}

extension EmbeddedPaymentElementTest: EmbeddedPaymentElementDelegate {
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is all the transparent padding expected? Would that be the merchant's UI/background in the real world?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Padding is expected; that is the default layoutMargins. Whether it should be transparent or appearance.colors.background, IDK. But the background color of the view can be set by the merchant so I think it doesn't matter?

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading