-
Notifications
You must be signed in to change notification settings - Fork 975
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
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason these aren't private (excluding |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
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) | ||
} | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason to not just remove
containerView
and just return it directly onview
? Guessing it's to avoid casting between UIView and the container view?