From 3b10f13f23c1523904f5a4cbae85412e548116d8 Mon Sep 17 00:00:00 2001 From: Florian Suess Date: Fri, 10 Apr 2026 12:46:05 +0100 Subject: [PATCH 1/2] Add networkBusinessProfile to SellerDetails for seller payment methods - Make SellerDetails fields (networkId, externalId, businessName) optional - Add networkBusinessProfile field to SellerDetails - Send business_name and network_business_profile in elements/sessions params - Add beta header when networkBusinessProfile is provided - Add mutual exclusion validation for sellerDetails vs paymentMethodConfigurationId - Add tests for new fields, beta header, and mutual exclusion Co-Authored-By: Claude Opus 4.6 (1M context) Committed-By-Agent: claude --- .../STPAPIClient+PaymentSheet.swift | 33 ++++++- .../PaymentSheet/PaymentSheetError.swift | 3 + .../PaymentSheetIntentConfiguration.swift | 28 +++++- .../STPAPIClient+PaymentSheetTest.swift | 98 +++++++++++++++++++ 4 files changed, 153 insertions(+), 9 deletions(-) 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..0fe283ad9036 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift @@ -58,14 +58,30 @@ 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 let networkId: String? + public let externalId: String? + public let businessName: String? + public let networkBusinessProfile: String? + /// Backwards-compatible initializer with required fields public init(networkId: String, externalId: String, businessName: String) { self.networkId = networkId self.externalId = externalId self.businessName = businessName + self.networkBusinessProfile = nil + } + + /// Initializer with all optional fields including networkBusinessProfile + 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 +277,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 { From 3fc8f2449cd85f9157a0c74a50469d773ead3f9e Mon Sep 17 00:00:00 2001 From: Florian Suess Date: Tue, 14 Apr 2026 15:48:53 +0100 Subject: [PATCH 2/2] Remove redundant backwards-compatible SellerDetails initializer The all-optional initializer already handles the 3-arg case since networkBusinessProfile defaults to nil. Co-Authored-By: Claude Opus 4.6 (1M context) Committed-By-Agent: claude --- .../PaymentSheet/PaymentSheetIntentConfiguration.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift index 0fe283ad9036..572e9ce872f6 100644 --- a/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift +++ b/StripePaymentSheet/StripePaymentSheet/Source/PaymentSheet/PaymentSheetIntentConfiguration.swift @@ -63,15 +63,6 @@ public extension PaymentSheet { public let businessName: String? public let networkBusinessProfile: String? - /// Backwards-compatible initializer with required fields - public init(networkId: String, externalId: String, businessName: String) { - self.networkId = networkId - self.externalId = externalId - self.businessName = businessName - self.networkBusinessProfile = nil - } - - /// Initializer with all optional fields including networkBusinessProfile public init( networkId: String? = nil, externalId: String? = nil,