From 5662dea9e56fe2085a9ab78880448e29f552f037 Mon Sep 17 00:00:00 2001 From: Yuki Date: Fri, 9 Aug 2024 14:44:46 -0700 Subject: [PATCH] =?UTF-8?q?Expand=20PaymentMethod=20in=20consumers/payment?= =?UTF-8?q?=5Fdetails/share=20request=20inste=E2=80=A6=20(#3901)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ad of using dummy empty PaymentMethod ## Motivation https://jira.corp.stripe.com/browse/RUN_MOBILESDK-3405 ## Testing Added assert and UI Test - it's indirect, but combined they do test what we want. Directly testing this seems very hard, since IDK how to simulate the Link signup flow outside a UI test. ## Changelog * [Fixed] Fixed an issue where signing up with Link and paying would vend an empty `STPPaymentMethod` object to an `IntentConfiguration` confirmHandler callback. --- CHANGELOG.md | 4 ++- .../PlaygroundController.swift | 5 ++++ .../PaymentSheetUITest.swift | 16 +++++++++++ .../project.pbxproj | 8 +++--- .../Link/PaymentDetailsShareResponse.swift | 27 ++++++++++++++++--- .../API Bindings/Link/STPAPIClient+Link.swift | 19 ++++++++----- .../PaymentSheet/PaymentSheet+API.swift | 5 ++-- .../API Bindings/STPAPIClient+Payments.swift | 1 - 8 files changed, 67 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 09422f33f27..94adb2a186c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ -## X.Y.Z 2024-xx-yy +## X.Y.Z 2024-XX-YY ### PaymentSheet +* [Fixed] Fixed an issue where signing up with Link and paying would vend an empty `STPPaymentMethod` object to an `IntentConfiguration` confirmHandler callback. * [Fixed] Fixed PaymentSheet.FlowController returning unlocalized labels for certain payment methods e.g. "AfterPay ClearPay" instead of "Afterpay" or "Clearpay" depending on locale. ### PaymentsUI * [Fixed] Fixed an issue where STPPaymentCardTextField wouldn't call its delegate `paymentCardTextFieldDidChange` method when the preferred card network changed. + ## 23.29.0 2024-08-05 ### PaymentSheet * [Fixed] Fixed a scroll issue with native 3DS2 authentication screen when the keyboard appears. diff --git a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift index 2b607b98bbb..122eecd1dd9 100644 --- a/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift +++ b/Example/PaymentSheet Example/PaymentSheet Example/PlaygroundController.swift @@ -622,6 +622,11 @@ extension PlaygroundController { func confirmHandler(_ paymentMethod: STPPaymentMethod, _ shouldSavePaymentMethod: Bool, _ intentCreationCallback: @escaping (Result) -> Void) { + // Sanity check the payment method + if paymentMethod.type == .card { + assert(paymentMethod.card != nil) + } + switch settings.integrationType { case .deferred_mp: // multiprocessor diff --git a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift index 9cb100b9394..38d2d5d6d3b 100644 --- a/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift +++ b/Example/PaymentSheet Example/PaymentSheetUITest/PaymentSheetUITest.swift @@ -2216,6 +2216,17 @@ class PaymentSheetLinkUITests: PaymentSheetUITestCase { XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) } + + func testLinkInlineSignup_deferred() { + var settings = PaymentSheetTestPlaygroundSettings.defaultValues() + settings.customerMode = .guest + settings.apmsEnabled = .on + settings.linkEnabled = .on + settings.integrationType = .deferred_ssc + loadPlayground(app, settings) + app.buttons["Present PaymentSheet"].waitForExistenceAndTap() + fillLinkAndPay(mode: .checkbox) + } // MARK: Link test helpers @@ -2266,6 +2277,11 @@ class PaymentSheetLinkUITests: PaymentSheetUITestCase { app.buttons["Confirm"].waitForExistenceAndTap() } XCTAssertTrue(app.staticTexts["Success!"].waitForExistence(timeout: 10.0)) + // Roundabout way to validate that signup completed successfully + let signupCompleteAnalytic = analyticsLog.first { payload in + payload["event"] as? String == "link.signup.complete" + } + XCTAssertNotNil(signupCompleteAnalytic) } private func assertLinkInlineSignupNotShown() { diff --git a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj index 453bfde4679..c89dac85fd3 100644 --- a/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj +++ b/StripePaymentSheet/StripePaymentSheet.xcodeproj/project.pbxproj @@ -60,7 +60,6 @@ 316B33122B5F171C0008D2E5 /* UserDefaults+StripePaymentSheetTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 316B33112B5F171C0008D2E5 /* UserDefaults+StripePaymentSheetTest.swift */; }; 31AD3BE72B0C2D080080C800 /* UIApplication+StripePaymentSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AD3BE62B0C2D080080C800 /* UIApplication+StripePaymentSheet.swift */; }; 31CDFC362BA8E66200B3DD91 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 31CDFC352BA8E66200B3DD91 /* PrivacyInfo.xcprivacy */; }; - 31D224F42B4637BA003E3D8B /* PaymentDetailsShareResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D224F32B4637BA003E3D8B /* PaymentDetailsShareResponse.swift */; }; 335A19D93A5979557DB4CA4D /* PaymentMethodElementWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5012364ED0F2EEC6EC2AB52 /* PaymentMethodElementWrapper.swift */; }; 34CF08CBC636F596B8BA4C12 /* TextFieldElement+CardTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00C7F5905759525C9BF8BD4 /* TextFieldElement+CardTest.swift */; }; 367BB57FA826A82EEF074A70 /* PayWithLinkWebController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 617C44F9338DE2E93E318291 /* PayWithLinkWebController.swift */; }; @@ -217,6 +216,7 @@ 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 */; }; + B662953E2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B662953D2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift */; }; B667BF0B2BF2B7C60050EFD8 /* RowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B667BF0A2BF2B7C60050EFD8 /* RowButton.swift */; }; B66D70FA2C54086600DECB3D /* HostedSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66D70F92C54086600DECB3D /* HostedSurface.swift */; }; B67D01B62C46FE9900ED8172 /* CVCReconfirmationVerticalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B67D01B52C46FE9900ED8172 /* CVCReconfirmationVerticalViewController.swift */; }; @@ -398,7 +398,6 @@ 316B33112B5F171C0008D2E5 /* UserDefaults+StripePaymentSheetTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+StripePaymentSheetTest.swift"; sourceTree = ""; }; 31AD3BE62B0C2D080080C800 /* UIApplication+StripePaymentSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "UIApplication+StripePaymentSheet.swift"; path = "StripePaymentSheet/Source/Categories/UIApplication+StripePaymentSheet.swift"; sourceTree = ""; }; 31CDFC352BA8E66200B3DD91 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; - 31D224F32B4637BA003E3D8B /* PaymentDetailsShareResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentDetailsShareResponse.swift; sourceTree = ""; }; 32332E0DB0AE12377EBDDEF1 /* PaymentSheetIntentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetIntentConfiguration.swift; sourceTree = ""; }; 32BDC53A88FB17F378C6B413 /* CardSectionWithScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardSectionWithScannerView.swift; sourceTree = ""; }; 33B8F21A22FA091BC9D2924B /* LinkStubs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkStubs.swift; sourceTree = ""; }; @@ -574,6 +573,7 @@ B64FEF112C0FAC1E00F7CA26 /* PaymentSheetVerticalViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentSheetVerticalViewControllerTest.swift; sourceTree = ""; }; B65B42962C013DED00EC565D /* PaymentMethodFormViewControllerTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentMethodFormViewControllerTest.swift; sourceTree = ""; }; B65FE7082BED33EA009A73FC /* VerticalPaymentMethodListViewControllerSnapshotTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalPaymentMethodListViewControllerSnapshotTest.swift; sourceTree = ""; }; + B662953D2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentDetailsShareResponse.swift; sourceTree = ""; }; B667BF0A2BF2B7C60050EFD8 /* RowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowButton.swift; sourceTree = ""; }; B667E074D30964FABC64B552 /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; B66D70F92C54086600DECB3D /* HostedSurface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostedSurface.swift; sourceTree = ""; }; @@ -921,8 +921,8 @@ 4208AD2E0A737F5E0F00DE48 /* ConsumerSession+LookupResponse.swift */, E2D61B52BFA201D25E8F6428 /* ConsumerSession+PublishableKey.swift */, 981F958E99945A0318D47BBF /* PaymentDetails.swift */, - 31D224F32B4637BA003E3D8B /* PaymentDetailsShareResponse.swift */, F1E614E8481658A027599A92 /* STPAPIClient+Link.swift */, + B662953D2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift */, 441C3414745D483C9A47ED0B /* VerificationSession.swift */, ); path = Link; @@ -1816,7 +1816,6 @@ B67D01B62C46FE9900ED8172 /* CVCReconfirmationVerticalViewController.swift in Sources */, 47B19F96CCEA290541E3B988 /* CardSectionElement.swift in Sources */, 04FEA90F2D0CB9D1C2029D21 /* CardSectionWithScannerView.swift in Sources */, - 31D224F42B4637BA003E3D8B /* PaymentDetailsShareResponse.swift in Sources */, 6BA8D3342B0C1F79008C51FF /* CVCRecollectionElement.swift in Sources */, 6BC19CCC2B16C4B2008E00C4 /* CVCRecollectionView.swift in Sources */, 68E3CF21A7E1525CA05BA260 /* ConnectionsElement.swift in Sources */, @@ -1896,6 +1895,7 @@ 436A212E364FD78C3745DDA3 /* CardScanningView.swift in Sources */, 19A6D9D9951E13377F305263 /* CircularButton.swift in Sources */, 6BA8D33A2B0C1FBF008C51FF /* CVCPaymentMethodInformationView.swift in Sources */, + B662953E2C63F6C2007B6B14 /* PaymentDetailsShareResponse.swift in Sources */, F79DBDF42E5C0ED6B6DDC246 /* ConfirmButton.swift in Sources */, 5C0D1B932954D0EF3F3A679F /* ManualEntryButton.swift in Sources */, 3DE056395324C5B3A5AAABDA /* PayWithLinkButton.swift in Sources */, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetailsShareResponse.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetailsShareResponse.swift index 703ff68f65e..4772bd04bb3 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetailsShareResponse.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/PaymentDetailsShareResponse.swift @@ -2,11 +2,32 @@ // PaymentDetailsShareResponse.swift // StripePaymentSheet // -// Created by David Estes on 1/3/24. +// Created by Yuki Tokuhiro on 8/7/24. // import Foundation +@_spi(STP) import StripePayments -struct PaymentDetailsShareResponse: Decodable { - let paymentMethod: String +final class PaymentDetailsShareResponse: NSObject, STPAPIResponseDecodable { + static func decodedObject(fromAPIResponse response: [AnyHashable: Any]?) -> Self? { + guard + let response, + let paymentMethodDict = response["payment_method"] as? [String: Any], + let paymentMethod = STPPaymentMethod.decodedObject(fromAPIResponse: paymentMethodDict) + else { + return nil + } + let paymentDetailsShareResponse = PaymentDetailsShareResponse( + paymentMethod: paymentMethod, + allResponseFields: response + ) + return .some(paymentDetailsShareResponse as! Self) + } + + let allResponseFields: [AnyHashable: Any] + let paymentMethod: STPPaymentMethod + init(paymentMethod: STPPaymentMethod, allResponseFields: [AnyHashable: Any]) { + self.paymentMethod = paymentMethod + self.allResponseFields = allResponseFields + } } diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift index 23a649e8e22..3aa434462ab 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/Link/STPAPIClient+Link.swift @@ -207,6 +207,7 @@ extension STPAPIClient { var parameters: [String: Any] = [ "credentials": ["consumer_session_client_secret": consumerSessionClientSecret], "request_surface": "ios_payment_element", + "expand": ["payment_method"], "id": id, ] @@ -214,12 +215,18 @@ extension STPAPIClient { parameters["payment_method_options"] = ["card": ["cvc": cvc]] } - post( - resource: endpoint, - parameters: parameters, - ephemeralKeySecret: nil, - completion: completion - ) + APIRequest.post( + with: self, + endpoint: endpoint, + parameters: parameters + ) { paymentDetailsShareResponse, _, error in + guard let paymentDetailsShareResponse else { + stpAssert(error != nil) + completion(.failure(error ?? NSError.stp_genericConnectionError())) + return + } + completion(.success(paymentDetailsShareResponse)) + } } func logout( diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift index eabedb81c81..49e0f7800d4 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheet+API.swift @@ -416,8 +416,8 @@ extension PaymentSheet { // If passthrough mode, share payment details linkAccount.sharePaymentDetails(id: paymentDetails.stripeID, cvc: paymentMethodParams.card?.cvc) { result in switch result { - case .success(let shareResponse): - confirmWithPaymentMethod(STPPaymentMethod(stripeId: shareResponse.paymentMethod, type: .card), linkAccount, shouldSave) + case .success(let paymentDetailsShareResponse): + confirmWithPaymentMethod(paymentDetailsShareResponse.paymentMethod, linkAccount, shouldSave) case .failure(let error): STPAnalyticsClient.sharedClient.logLinkSharePaymentDetailsFailure(error: error) // If this fails, confirm directly @@ -447,7 +447,6 @@ extension PaymentSheet { switch result { case .success: STPAnalyticsClient.sharedClient.logLinkSignupComplete() - createPaymentDetailsAndConfirm(linkAccount, intentConfirmParams.paymentMethodParams, intentConfirmParams.saveForFutureUseCheckboxState == .selected) case .failure(let error as NSError): STPAnalyticsClient.sharedClient.logLinkSignupFailure(error: error) diff --git a/StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Payments.swift b/StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Payments.swift index 01bf374a526..1ad35d73d4d 100644 --- a/StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Payments.swift +++ b/StripePayments/StripePayments/Source/API Bindings/STPAPIClient+Payments.swift @@ -826,7 +826,6 @@ extension STPAPIClient { ) { paymentMethod, _, error in completion(paymentMethod, error) } - } /// Creates a PaymentMethod object with the provided params object.