Skip to content

Commit

Permalink
Merge branch 'master' into releases/23.27.0
Browse files Browse the repository at this point in the history
  • Loading branch information
porter-stripe committed Apr 9, 2024
2 parents 1e5ac43 + 95eee8f commit febc79a
Show file tree
Hide file tree
Showing 12 changed files with 365 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class PaymentSheetStandardUITests: PaymentSheetUITestCase {
let sessionID = analyticsLog.first![string: "session_id"]
XCTAssertTrue(!sessionID!.isEmpty)
for analytic in analyticsLog {
if analytic[string: "event"] == "stripeios.payment_intent_confirmation" {
if analytic[string: "event"]!.starts(with: "stripeios.") {
continue
}
XCTAssertEqual(analytic[string: "session_id"], sessionID)
Expand Down Expand Up @@ -1459,8 +1459,8 @@ class PaymentSheetDeferredUITests: PaymentSheetUITestCase {
XCTAssertTrue(successText.waitForExistence(timeout: 10.0))

XCTAssertEqual(
analyticsLog.suffix(6).map({ $0[string: "event"] }),
["mc_form_interacted", "mc_card_number_completed", "mc_confirm_button_tapped", "stripeios.payment_method_creation", "stripeios.payment_intent_confirmation", "mc_complete_payment_newpm_success"]
analyticsLog.suffix(8).map({ $0[string: "event"] }),
["mc_form_interacted", "mc_card_number_completed", "mc_confirm_button_tapped", "stripeios.payment_method_creation", "stripeios.paymenthandler.confirm.started", "stripeios.payment_intent_confirmation", "stripeios.paymenthandler.confirm.finished", "mc_complete_payment_newpm_success"]
)

// Make sure they all have the same session id
Expand Down
153 changes: 153 additions & 0 deletions Stripe/StripeiOSTests/STPPaymentHandlerFunctionalTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

@testable import Stripe
@_spi(STP) @testable import StripeCore
@_spi(STP) @testable import StripePayments
@_spi(STP) @testable import StripePaymentsTestUtils
import XCTest
Expand Down Expand Up @@ -190,4 +191,156 @@ final class STPPaymentHandlerFunctionalSwiftTest: XCTestCase, STPAuthenticationC
}
self.waitForExpectations(timeout: 10)
}

// MARK: - Test payment handler sends analytics

func test_confirm_payment_intent_sends_analytic() {
// Confirming a hardcoded already-confirmed PI with invalid params...
let paymentIntentParams = STPPaymentIntentParams(clientSecret: "pi_3P20wFFY0qyl6XeW0dSOQ6W7_secret_9V8GkrCOt1MEW8SBmAaGnmT6A", paymentMethodType: .card)
let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.confirmPayment(paymentIntentParams, with: self) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7")
XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_3P20wFFY0qyl6XeW0dSOQ6W7")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "payment_intent_unexpected_state")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}

func test_confirm_setup_intent_sends_analytic() {
let setupIntentParams = STPSetupIntentConfirmParams(clientSecret: "seti_1P1xLBFY0qyl6XeWc7c2LrMK_secret_PrgithiYFFPH0NVGP1BK7Oy9OU3mrDT", paymentMethodType: .card)
// Confirming a hardcoded already-confirmed SI with invalid params...
setupIntentParams.paymentMethodParams = STPPaymentMethodParams(type: .card)

let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.confirmSetupIntent(setupIntentParams, with: self) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK")
XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_1P1xLBFY0qyl6XeWc7c2LrMK")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "parameter_missing")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}

func test_handle_next_action_payment_intent_sends_analytic() {
// Calling handleNextAction(forPayment:) with an invalid PI client secret...
let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.handleNextAction(forPayment: "pi_3P232pFY0qyl6XeW0FFRtE0A_secret_foo", with: self, returnURL: nil) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_3P232pFY0qyl6XeW0FFRtE0A")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_3P232pFY0qyl6XeW0FFRtE0A")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "payment_intent_invalid_parameter")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}

func test_handle_next_action_2_payment_intent_sends_analytic() {
// Calling handleNextAction(for:) with a STPPaymentIntent w/ an unknown next action...
let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
var piJSON = STPTestUtils.jsonNamed("PaymentIntent")
piJSON![jsonDict: "next_action"]!["type"] = "foo"
let paymentIntent = STPPaymentIntent.decodedObject(fromAPIResponse: piJSON)!

let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.handleNextAction(for: paymentIntent, with: self, returnURL: nil) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "pi_1Cl15wIl4IdHmuTbCWrpJXN6")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "pi_1Cl15wIl4IdHmuTbCWrpJXN6")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "0")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}

func test_handle_next_action_setup_intent_sends_analytic() {
// Calling handleNextAction(forSetupIntent:) with an invalid SI client secret...
let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.handleNextAction(forSetupIntent: "seti_3P232pFY0qyl6XeW0FFRtE0A_secret_foo", with: self, returnURL: nil) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_3P232pFY0qyl6XeW0FFRtE0A")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_3P232pFY0qyl6XeW0FFRtE0A")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "invalid_request_error")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "resource_missing")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}

func test_handle_next_action_2_setup_intent_sends_analytic() {
// Calling handleNextAction(for:) with a STPSetupIntent w/ an unknown next action...
let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
var siJSON = STPTestUtils.jsonNamed("SetupIntent")!
siJSON[jsonDict: "next_action"]!["type"] = "foo"
siJSON[jsonDict: "next_action"]!["type"] = "foo"
siJSON["payment_method"] = STPTestUtils.jsonNamed("CardPaymentMethod")!
let setupIntent = STPSetupIntent.decodedObject(fromAPIResponse: siJSON)!

let paymentHandler = STPPaymentHandler(apiClient: STPAPIClient(publishableKey: STPTestingDefaultPublishableKey))
let analyticsClient = STPAnalyticsClient()
paymentHandler.analyticsClient = analyticsClient
paymentHandler.handleNextAction(for: setupIntent, with: self, returnURL: nil) { (_, _, _) in
// ...should send these analytics
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, "seti_123456789")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerHandleNextActionFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, "seti_123456789")
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "0")
paymentHandlerExpectation.fulfill()
}
waitForExpectations(timeout: 10)
}
}
13 changes: 13 additions & 0 deletions Stripe/StripeiOSTests/STPPaymentHandlerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,21 @@ class STPPaymentHandlerStubbedTests: STPNetworkStubbingTestCase {

let paymentHandlerExpectation = expectation(description: "paymentHandlerExpectation")
STPPaymentHandler.shared().checkCanPresentInTest = true
let analyticsClient = STPAnalyticsClient()
STPPaymentHandler.sharedHandler.analyticsClient = analyticsClient
STPPaymentHandler.shared().confirmPayment(paymentIntentParams, with: self) {
(status, paymentIntent, error) in
let firstAnalytic = analyticsClient._testLogHistory.first
XCTAssertEqual(firstAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmStarted.rawValue)
XCTAssertEqual(firstAnalytic?["intent_id"] as? String, paymentIntentParams.stripeId)
XCTAssertEqual(firstAnalytic?["payment_method_type"] as? String, "card")
let lastAnalytic = analyticsClient._testLogHistory.last
XCTAssertEqual(lastAnalytic?["event"] as? String, STPAnalyticEvent.paymentHandlerConfirmFinished.rawValue)
XCTAssertEqual(lastAnalytic?["intent_id"] as? String, paymentIntentParams.stripeId)
XCTAssertEqual(lastAnalytic?["status"] as? String, "failed")
XCTAssertEqual(lastAnalytic?["payment_method_type"] as? String, "card")
XCTAssertEqual(lastAnalytic?["error_type"] as? String, "STPPaymentHandlerErrorDomain")
XCTAssertEqual(lastAnalytic?["error_code"] as? String, "8")
XCTAssertTrue(status == .failed)
XCTAssertNotNil(paymentIntent)
XCTAssertNotNil(error)
Expand Down
4 changes: 4 additions & 0 deletions StripeCore/StripeCore/Source/Analytics/STPAnalyticEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import Foundation
case _3DS2ChallengeFlowCompleted = "stripeios.3ds2_challenge_flow_completed"
case _3DS2ChallengeFlowErrored = "stripeios.3ds2_challenge_flow_errored"
case _3DS2RedirectUserCanceled = "stripeios.3ds2_redirect_canceled"
case paymentHandlerConfirmStarted = "stripeios.paymenthandler.confirm.started"
case paymentHandlerConfirmFinished = "stripeios.paymenthandler.confirm.finished"
case paymentHandlerHandleNextActionStarted = "stripeios.paymenthandler.handle_next_action.started"
case paymentHandlerHandleNextActionFinished = "stripeios.paymenthandler.handle_next_action.finished"

// MARK: - Card Metadata
case cardMetadataLoadedTooSlow = "stripeios.card_metadata_loaded_too_slow"
Expand Down
12 changes: 8 additions & 4 deletions StripeCore/StripeCore/Source/Categories/Dictionary+Stripe.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ extension Dictionary {
return newDict
}

@inlinable public mutating func mergeAssertingOnOverwrites(_ other: [Key: Value]) {
public mutating func mergeAssertingOnOverwrites(_ other: [Key: Value]) {
merge(other) { a, b in
#if DEBUG
assertionFailure("Dictionary merge is overwriting a key with values: \(a) and \(b)!")
#endif
stpAssertionFailure("Dictionary merge is overwriting a key with values: \(a) and \(b)!")
return a
}
}

public func mergingAssertingOnOverwrites<S>(_ other: S) -> [Key: Value] where S: Sequence, S.Element == (Key, Value) {
merging(other) { a, b in
stpAssertionFailure("Dictionary merge is overwriting a key with values: \(a) and \(b)!")
return a
}
}
}

extension Dictionary where Value == Any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@ final class PayWithLinkController {
self.paymentHandler = .init(apiClient: configuration.apiClient)
}

func present(completion: @escaping CompletionBlock) {
guard
let keyWindow = UIApplication.shared.stp_hackilyFumbleAroundUntilYouFindAKeyWindow(),
let presentedViewController = keyWindow.findTopMostPresentedViewController()
else {
assertionFailure("No key window with view controller found")
return
}

present(from: presentedViewController, completion: completion)
}

func present(
from presentingController: UIViewController,
completion: @escaping CompletionBlock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ protocol PaymentMethodTypeCollectionViewDelegate: AnyObject {
/// For internal SDK use only
@objc(STP_Internal_PaymentMethodTypeCollectionView)
class PaymentMethodTypeCollectionView: UICollectionView {
enum Error: Swift.Error {
case unableToDequeueReusableCell
}

// MARK: - Constants
internal static let paymentMethodLogoSize: CGSize = CGSize(width: UIView.noIntrinsicMetric, height: 12)
internal static let cellHeight: CGFloat = 52
Expand All @@ -44,6 +48,7 @@ class PaymentMethodTypeCollectionView: UICollectionView {
isPaymentSheet: Bool = false,
delegate: PaymentMethodTypeCollectionViewDelegate
) {
// Break loudly! This will cause other parts of the code to crash if not true
assert(!paymentMethodTypes.isEmpty, "At least one payment method type must be provided.")

self.paymentMethodTypes = paymentMethodTypes
Expand Down Expand Up @@ -106,7 +111,10 @@ extension PaymentMethodTypeCollectionView: UICollectionViewDataSource, UICollect
as? PaymentMethodTypeCollectionView.PaymentTypeCell,
let appearance = (collectionView as? PaymentMethodTypeCollectionView)?.appearance
else {
assertionFailure()
let errorAnalytic = ErrorAnalytic(event: .unexpectedPaymentSheetError,
error: Error.unableToDequeueReusableCell)
STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic)
stpAssertionFailure()
return UICollectionViewCell()
}
cell.paymentMethodType = paymentMethodTypes[indexPath.item]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ extension PaymentSheet {
switch confirmOption {
case .wallet:
let linkController = PayWithLinkController(intent: intent, configuration: configuration)
linkController.present(completion: completion)
linkController.present(from: authenticationContext.authenticationPresentingViewController(),
completion: completion)
case .signUp(let linkAccount, let phoneNumber, let consentAction, let legalName, let intentConfirmParams):
linkAccount.signUp(with: phoneNumber, legalName: legalName, consentAction: consentAction) { result in
UserDefaults.standard.markLinkAsUsed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import PassKit
import UIKit

enum PaymentSheetUI {
enum Error: Swift.Error {
case switchContentIfNecessaryStateInvalid
}
/// The padding between views in the sheet e.g., between the bottom of the form and the Pay button
static let defaultPadding: CGFloat = 20

Expand Down Expand Up @@ -60,7 +63,18 @@ extension UIViewController {
func switchContentIfNecessary(
to toVC: UIViewController, containerView: DynamicHeightContainerView
) {
assert(children.count <= 1)
if children.count > 1 {
let from_vc_name = NSStringFromClass(children.first!.classForCoder)
let to_vc_name = NSStringFromClass(toVC.classForCoder)

let errorAnalytic = ErrorAnalytic(event: .unexpectedPaymentSheetError,
error: PaymentSheetUI.Error.switchContentIfNecessaryStateInvalid,
additionalNonPIIParams: ["from_vc": from_vc_name,
"to_vc": to_vc_name,
])
STPAnalyticsClient.sharedClient.log(analytic: errorAnalytic)
}
stpAssert(children.count <= 1)
// Swap out child view controllers if necessary
if let fromVC = children.first {
guard fromVC != toVC else {
Expand Down

0 comments on commit febc79a

Please sign in to comment.