Skip to content

Commit

Permalink
Embedded update pt 2 - update view (#4150)
Browse files Browse the repository at this point in the history
## Summary
Previous PR: #4141

Makes EmbeddedPaymentElement view a view that contains embedded view so
we can swap to the updated embedded view with an animation.


https://github.com/user-attachments/assets/533e0fb1-c848-4fea-9793-30347bbd90f1


Still to come:

- Restore previous customer input + E2E tests (e.g. load PI -> fill out
card form -> update to SI -> expect form to be preserved but w/o
checkbox)
- Make confirm handle in-flight and failed update calls.
- (Bonus) Cancel network calls etc. from previous update to reduce
battery/network usage. Can apply this to FC.update as well.

## Motivation
https://jira.corp.stripe.com/browse/MOBILESDK-2583

## Testing
See snapshot test. 

## Changelog
Not user facing

---------

Co-authored-by: Nick Porter <88012362+porter-stripe@users.noreply.github.com>
  • Loading branch information
yuki-stripe and porter-stripe authored Oct 21, 2024
1 parent 2d1e81f commit 0995ef4
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 5 deletions.
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
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 = {}
private var view: EmbeddedPaymentMethodsView
private var bottomAnchorConstraint: NSLayoutConstraint!

init(embeddedPaymentMethodsView: EmbeddedPaymentMethodsView) {
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: EmbeddedPaymentMethodsView) {
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 {
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 0995ef4

Please sign in to comment.