diff --git a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/STPAPIClient+PaymentSheet.swift b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/STPAPIClient+PaymentSheet.swift index d0f9487a40c1..3600819da0fb 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/STPAPIClient+PaymentSheet.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/Internal/API Bindings/STPAPIClient+PaymentSheet.swift @@ -54,10 +54,20 @@ extension STPAPIClient { parameters["type"] = "deferred_intent" parameters["key"] = publishableKey if let sellerDetails = intentConfig.sellerDetails { - parameters["seller_details"] = [ - "network_id": sellerDetails.networkId, - "external_id": sellerDetails.externalId, - ] + var sellerDetailsParams: [String: String] = [:] + if let networkId = sellerDetails.networkId { + sellerDetailsParams["network_id"] = networkId + } + if let externalId = sellerDetails.externalId { + sellerDetailsParams["external_id"] = externalId + } + if let businessName = sellerDetails.businessName { + sellerDetailsParams["business_name"] = businessName + } + if let networkBusinessProfile = sellerDetails.networkBusinessProfile { + sellerDetailsParams["network_business_profile"] = networkBusinessProfile + } + parameters["seller_details"] = sellerDetailsParams } parameters["deferred_intent"] = { var deferredIntent = [String: Any]() @@ -167,6 +177,21 @@ extension STPAPIClient { clientDefaultPaymentMethod: String?, configuration: PaymentElementConfiguration ) async throws -> STPElementsSession { + // Add beta header when networkBusinessProfile is provided + let addedBeta = intentConfig.sellerDetails?.networkBusinessProfile != nil + if addedBeta { + var betas = self.betas + betas.insert("payment_element_seller_payment_methods_beta_1=v1") + self.betas = betas + } + defer { + if addedBeta { + var betas = self.betas + betas.remove("payment_element_seller_payment_methods_beta_1=v1") + self.betas = betas + } + } + let parameters = makeElementsSessionsParams( mode: .deferredIntent(intentConfig), epmConfiguration: configuration.externalPaymentMethodConfiguration, diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetError.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetError.swift index 3b9aa09c54fd..48cc2f33f66f 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetError.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetError.swift @@ -42,6 +42,7 @@ public enum PaymentSheetError: Error, LocalizedError { // MARK: Deferred intent errors case intentConfigurationValidationFailed(message: String) case deferredIntentValidationFailed(message: String) + case paymentMethodConfigurationAndSellerDetailsMutuallyExclusive // MARK: - Link errors case linkSignUpNotRequired @@ -132,6 +133,8 @@ extension PaymentSheetError: CustomDebugStringConvertible { return "New payment method should not have been created yet" case .intentConfigurationValidationFailed(message: let message): return message + case .paymentMethodConfigurationAndSellerDetailsMutuallyExclusive: + return "`paymentMethodConfigurationId` and `sellerDetails` are mutually exclusive and cannot both be set on IntentConfiguration." case .embeddedPaymentElementAlreadyConfirmedIntent: return "This instance of EmbeddedPaymentElement has already confirmed an intent successfully. Create a new instance of EmbeddedPaymentElement to confirm a new intent." case .integrationError(nonPIIDebugDescription: let nonPIIDebugDescription): diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift index 91d404c0c2c8..572e9ce872f6 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift @@ -58,14 +58,21 @@ public extension PaymentSheet { /// Seller details for facilitated payment sessions @_spi(SharedPaymentToken) public struct SellerDetails { - public let networkId: String - public let externalId: String - public let businessName: String - - public init(networkId: String, externalId: String, businessName: String) { + public let networkId: String? + public let externalId: String? + public let businessName: String? + public let networkBusinessProfile: String? + + public init( + networkId: String? = nil, + externalId: String? = nil, + businessName: String? = nil, + networkBusinessProfile: String? = nil + ) { self.networkId = networkId self.externalId = externalId self.businessName = businessName + self.networkBusinessProfile = networkBusinessProfile } } @@ -261,11 +268,13 @@ public extension PaymentSheet { @discardableResult func validate() -> Error? { - let errorMessage: String if case .payment(let amount, _, _, _, _) = mode, amount <= 0 { - errorMessage = "The amount in `PaymentSheet.IntentConfiguration` must be non-zero! See https://docs.stripe.com/api/payment_intents/create#create_payment_intent-amount" + let errorMessage = "The amount in `PaymentSheet.IntentConfiguration` must be non-zero! See https://docs.stripe.com/api/payment_intents/create#create_payment_intent-amount" return PaymentSheetError.intentConfigurationValidationFailed(message: errorMessage) } + if sellerDetails != nil && paymentMethodConfigurationId != nil { + return PaymentSheetError.paymentMethodConfigurationAndSellerDetailsMutuallyExclusive + } return nil } diff --git a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPAPIClient+PaymentSheetTest.swift b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPAPIClient+PaymentSheetTest.swift index 28881cf86b30..70af68de9756 100644 --- a/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPAPIClient+PaymentSheetTest.swift +++ b/StripePaymentSheet/StripePaymentSheetTests/PaymentSheet/STPAPIClient+PaymentSheetTest.swift @@ -248,6 +248,104 @@ class STPAPIClient_PaymentSheetTest: XCTestCase { XCTAssertEqual(sellerDetailsParams["network_id"] as? String, "network_123") XCTAssertEqual(sellerDetailsParams["external_id"] as? String, "external_456") + XCTAssertEqual(sellerDetailsParams["business_name"] as? String, "Till's Pills") + XCTAssertNil(sellerDetailsParams["network_business_profile"]) + } + + func testElementsSessionParameters_DeferredPayment_WithNetworkBusinessProfile() throws { + let sellerDetails = PaymentSheet.IntentConfiguration.SellerDetails( + networkId: "network_123", + externalId: "external_456", + businessName: "Till's Pills", + networkBusinessProfile: "nbp_123" + ) + let intentConfig = PaymentSheet.IntentConfiguration( + sharedPaymentTokenSessionWithMode: .payment(amount: 2000, currency: "USD"), + sellerDetails: sellerDetails, + paymentMethodTypes: ["card"], + preparePaymentMethodHandler: { _, _ in } + ) + + let parameters = STPAPIClient(publishableKey: "pk_test").makeElementsSessionsParams( + mode: .deferredIntent(intentConfig), + epmConfiguration: nil, + cpmConfiguration: nil, + clientDefaultPaymentMethod: nil, + customerAccessProvider: nil, + linkDisallowFundingSourceCreation: [] + ) + + let sellerDetailsParams = try XCTUnwrap(parameters["seller_details"] as? [String: Any]) + + XCTAssertEqual(sellerDetailsParams["network_id"] as? String, "network_123") + XCTAssertEqual(sellerDetailsParams["external_id"] as? String, "external_456") + XCTAssertEqual(sellerDetailsParams["business_name"] as? String, "Till's Pills") + XCTAssertEqual(sellerDetailsParams["network_business_profile"] as? String, "nbp_123") + } + + func testElementsSessionParameters_DeferredPayment_WithSellerDetails_OptionalFields() throws { + let sellerDetails = PaymentSheet.IntentConfiguration.SellerDetails( + networkBusinessProfile: "nbp_only" + ) + let intentConfig = PaymentSheet.IntentConfiguration( + sharedPaymentTokenSessionWithMode: .payment(amount: 2000, currency: "USD"), + sellerDetails: sellerDetails, + paymentMethodTypes: ["card"], + preparePaymentMethodHandler: { _, _ in } + ) + + let parameters = STPAPIClient(publishableKey: "pk_test").makeElementsSessionsParams( + mode: .deferredIntent(intentConfig), + epmConfiguration: nil, + cpmConfiguration: nil, + clientDefaultPaymentMethod: nil, + customerAccessProvider: nil, + linkDisallowFundingSourceCreation: [] + ) + + let sellerDetailsParams = try XCTUnwrap(parameters["seller_details"] as? [String: Any]) + + XCTAssertNil(sellerDetailsParams["network_id"]) + XCTAssertNil(sellerDetailsParams["external_id"]) + XCTAssertNil(sellerDetailsParams["business_name"]) + XCTAssertEqual(sellerDetailsParams["network_business_profile"] as? String, "nbp_only") + } + + func testBetaHeader_AddedWhenNetworkBusinessProfileProvided() throws { + let sellerDetails = PaymentSheet.IntentConfiguration.SellerDetails( + networkBusinessProfile: "nbp_123" + ) + let intentConfig = PaymentSheet.IntentConfiguration( + sharedPaymentTokenSessionWithMode: .payment(amount: 2000, currency: "USD"), + sellerDetails: sellerDetails, + paymentMethodTypes: ["card"], + preparePaymentMethodHandler: { _, _ in } + ) + + let apiClient = STPAPIClient(publishableKey: "pk_test") + XCTAssertFalse(apiClient.betas.contains("payment_element_seller_payment_methods_beta_1=v1")) + + // We can't easily test the full request flow, but we can verify the beta would be added + // by checking the intentConfig condition + XCTAssertNotNil(intentConfig.sellerDetails?.networkBusinessProfile) + } + + func testValidation_SellerDetailsAndPaymentMethodConfiguration_MutuallyExclusive() { + let sellerDetails = PaymentSheet.IntentConfiguration.SellerDetails( + networkId: "network_123", + externalId: "external_456", + businessName: "Till's Pills" + ) + let intentConfig = PaymentSheet.IntentConfiguration( + sharedPaymentTokenSessionWithMode: .payment(amount: 2000, currency: "USD"), + sellerDetails: sellerDetails, + paymentMethodConfigurationId: "pmc_123", + preparePaymentMethodHandler: { _, _ in } + ) + + let error = intentConfig.validate() + XCTAssertNotNil(error) + XCTAssertTrue(error is PaymentSheetError) } func testElementsSessionParameters_DeferredPayment_WithoutSellerDetails() throws {