Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BANKCON-15711] Pass billing address and email address to payment details API #4148

Merged
merged 4 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions StripeCore/StripeCore.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
492039932CA47A8600CE2072 /* ElementsSessionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492039922CA47A8600CE2072 /* ElementsSessionContext.swift */; };
493B33062CA3015600E3622F /* LinkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 493B33052CA3015600E3622F /* LinkMode.swift */; };
49ECDA412CA340E100F647F0 /* AsyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49ECDA402CA340E100F647F0 /* AsyncTests.swift */; };
49F3828D2CC02D43001CE69A /* BillingAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49F3828C2CC02D43001CE69A /* BillingAddress.swift */; };
4B2FAC57E03D8654A177C408 /* Dictionary+Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7727AEEFD2FC880BADDA1872 /* Dictionary+Stripe.swift */; };
53D46A03B77577EE21F4B166 /* StripeCodableTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FCE36551600C3E53BEAF8F0 /* StripeCodableTest.swift */; };
552DA7969984C443617DBC3E /* STPMultipartFormDataPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C72BA9C44FF60A0E7BEF76 /* STPMultipartFormDataPart.swift */; };
Expand Down Expand Up @@ -235,6 +236,7 @@
49424775D3233411D9C2473B /* StripeCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StripeCodable.swift; sourceTree = "<group>"; };
49538DBF8457D96707A2DA56 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
49ECDA402CA340E100F647F0 /* AsyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncTests.swift; sourceTree = "<group>"; };
49F3828C2CC02D43001CE69A /* BillingAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BillingAddress.swift; sourceTree = "<group>"; };
4A8030BF88608CA86E295F18 /* Enums+CustomStringConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Enums+CustomStringConvertible.swift"; sourceTree = "<group>"; };
4C51E3FA5EE3587BB7BBC634 /* STPError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = STPError.swift; sourceTree = "<group>"; };
4EC3BCEEECB3E1485B18F0C4 /* FinancialConnectionsSDKInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinancialConnectionsSDKInterface.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -456,6 +458,7 @@
6A05FB4A2BCF245C0001D128 /* FinancialConnectionsEvent.swift */,
493B33052CA3015600E3622F /* LinkMode.swift */,
492039922CA47A8600CE2072 /* ElementsSessionContext.swift */,
49F3828C2CC02D43001CE69A /* BillingAddress.swift */,
);
path = "Connections Bindings";
sourceTree = "<group>";
Expand Down Expand Up @@ -1026,6 +1029,7 @@
A62AEDF871AC89489FE19A13 /* ServerErrorMapper.swift in Sources */,
B6DBB2BF2BA8C4E400783D15 /* STPAnalyticsClient+Error.swift in Sources */,
6A05FB452BCF24100001D128 /* FinancialConnectionsSDKResult.swift in Sources */,
49F3828D2CC02D43001CE69A /* BillingAddress.swift in Sources */,
62FD088E003BE06F5413FB4F /* StripeCoreBundleLocator.swift in Sources */,
17CE96B50813CF626293CBF9 /* URLEncoder.swift in Sources */,
0709F5D265CC641E6DE1011D /* URLSession+Retry.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// BillingAddress.swift
// StripeCore
//
// Created by Mat Schmid on 2024-10-16.
//

import Foundation

@_spi(STP) public struct BillingAddress: Encodable {
let name: String?
let line1: String?
let line2: String?
let city: String?
let state: String?
let postalCode: String?
let countryCode: String?

@_spi(STP) public init(
name: String? = nil,
line1: String? = nil,
line2: String? = nil,
city: String? = nil,
state: String? = nil,
postalCode: String? = nil,
countryCode: String? = nil
) {
self.name = name
self.line1 = line1
self.line2 = line2
self.city = city
self.state = state
self.postalCode = postalCode
self.countryCode = countryCode
}

enum CodingKeys: String, CodingKey {
case name
case line1 = "line_1"
case line2 = "line_2"
case city = "locality"
case state = "administrative_area"
case postalCode = "postal_code"
case countryCode = "country_code"
Comment on lines +38 to +44
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// Custom encoder to only encode non-nil & non-empty properties.
@_spi(STP) public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let name, !name.isEmpty { try container.encode(name, forKey: .name) }
if let line1, !line1.isEmpty { try container.encode(line1, forKey: .line1) }
if let line2, !line2.isEmpty { try container.encode(line2, forKey: .line2) }
if let city, !city.isEmpty { try container.encode(city, forKey: .city) }
if let state, !state.isEmpty { try container.encode(state, forKey: .state) }
if let postalCode, !postalCode.isEmpty { try container.encode(postalCode, forKey: .postalCode) }
if let countryCode, !countryCode.isEmpty { try container.encode(countryCode, forKey: .countryCode) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ import Foundation
@_spi(STP) public let prefillDetails: PrefillDetails?
@_spi(STP) public let intentId: IntentID?
@_spi(STP) public let linkMode: LinkMode?
@_spi(STP) public let billingAddress: BillingAddress?

@_spi(STP) public init(
amount: Int?,
currency: String?,
prefillDetails: PrefillDetails?,
intentId: IntentID?,
linkMode: LinkMode?
linkMode: LinkMode?,
billingAddress: BillingAddress?
) {
self.amount = amount
self.currency = currency
self.prefillDetails = prefillDetails
self.intentId = intentId
self.linkMode = linkMode
self.billingAddress = billingAddress
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import Foundation
@_spi(STP) import StripeCore

final class FinancialConnectionsAPIClient {
private enum EncodingError: Error {
case cannotCastToDictionary
}

let backingAPIClient: STPAPIClient

var isLinkWithStripe: Bool = false
Expand Down Expand Up @@ -74,6 +78,17 @@ final class FinancialConnectionsAPIClient {
}
return promise
}

static func encodeAsParameters(_ value: any Encodable) throws -> [String: Any] {
let jsonData = try JSONEncoder().encode(value)
let jsonObject = try JSONSerialization.jsonObject(with: jsonData)

if let dictionary = jsonObject as? [String: Any] {
return dictionary
} else {
throw EncodingError.cannotCastToDictionary
}
}
}

protocol FinancialConnectionsAPI {
Expand Down Expand Up @@ -230,7 +245,9 @@ protocol FinancialConnectionsAPI {

func paymentDetails(
consumerSessionClientSecret: String,
bankAccountId: String
bankAccountId: String,
billingAddress: BillingAddress?,
billingEmail: String?
) -> Future<FinancialConnectionsPaymentDetails>

func sharePaymentDetails(
Expand Down Expand Up @@ -967,9 +984,11 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {

func paymentDetails(
consumerSessionClientSecret: String,
bankAccountId: String
bankAccountId: String,
billingAddress: BillingAddress?,
billingEmail: String?
) -> Future<FinancialConnectionsPaymentDetails> {
let parameters: [String: Any] = [
var parameters: [String: Any] = [
"request_surface": requestSurface,
"credentials": [
"consumer_session_client_secret": consumerSessionClientSecret
Expand All @@ -979,6 +998,22 @@ extension FinancialConnectionsAPIClient: FinancialConnectionsAPI {
],
"type": "bank_account",
]

if let billingAddress {
do {
let encodedBillingAddress = try Self.encodeAsParameters(billingAddress)
parameters["billing_address"] = encodedBillingAddress
} catch let error {
let promise = Promise<FinancialConnectionsPaymentDetails>()
promise.reject(with: error)
return promise
}
}

if let billingEmail, !billingEmail.isEmpty {
parameters["billing_email_address"] = billingEmail
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

return post(
resource: APIEndpointPaymentDetails,
parameters: parameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,10 +508,14 @@ extension NativeFlowController {

// Bank account details extraction for the linked bank
var bankAccountDetails: BankAccountDetails?
let linkMode = dataManager.elementsSessionContext?.linkMode
let elementsSessionContext = dataManager.elementsSessionContext
let linkMode = elementsSessionContext?.linkMode
let email = dataManager.consumerSession?.emailAddress
dataManager.createPaymentDetails(
consumerSessionClientSecret: consumerSession.clientSecret,
bankAccountId: bankAccountId
bankAccountId: bankAccountId,
billingAddress: elementsSessionContext?.billingAddress,
billingEmail: email
)
.chained { [weak self] paymentDetails -> Future<PaymentMethodIDProvider> in
guard let self else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ protocol NativeFlowDataManager: AnyObject {

func createPaymentDetails(
consumerSessionClientSecret: String,
bankAccountId: String
bankAccountId: String,
billingAddress: BillingAddress?,
billingEmail: String?
) -> Future<FinancialConnectionsPaymentDetails>
func createPaymentMethod(
consumerSessionClientSecret: String,
Expand Down Expand Up @@ -140,11 +142,15 @@ class NativeFlowAPIDataManager: NativeFlowDataManager {

func createPaymentDetails(
consumerSessionClientSecret: String,
bankAccountId: String
bankAccountId: String,
billingAddress: BillingAddress?,
billingEmail: String?
) -> Future<FinancialConnectionsPaymentDetails> {
apiClient.paymentDetails(
consumerSessionClientSecret: consumerSessionClientSecret,
bankAccountId: bankAccountId
bankAccountId: bankAccountId,
billingAddress: billingAddress,
billingEmail: billingEmail
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,12 @@ class EmptyFinancialConnectionsAPIClient: FinancialConnectionsAPI {
return Promise<StripeFinancialConnections.AttachLinkConsumerToLinkAccountSessionResponse>()
}

func paymentDetails(consumerSessionClientSecret: String, bankAccountId: String) -> StripeCore.Future<StripeFinancialConnections.FinancialConnectionsPaymentDetails> {
func paymentDetails(
consumerSessionClientSecret: String,
bankAccountId: String,
billingAddress: BillingAddress?,
billingEmail: String?
) -> StripeCore.Future<StripeFinancialConnections.FinancialConnectionsPaymentDetails> {
Promise<StripeFinancialConnections.FinancialConnectionsPaymentDetails>()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,46 @@ class FinancialConnectionsAPIClientTests: XCTestCase {

XCTAssertNil(apiClient.consumerPublishableKeyProvider(canUseConsumerKey: false))
}

func testBillingAddressEncodedAsParameters() throws {
let billingAddress = BillingAddress(
name: "Bobby Tables",
line1: "123 Fake St",
line2: nil,
mats-stripe marked this conversation as resolved.
Show resolved Hide resolved
city: "Utopia",
state: "CA",
postalCode: "90210",
countryCode: "US"
)
let encodedBillingAddress = try FinancialConnectionsAPIClient.encodeAsParameters(billingAddress)

XCTAssertEqual(encodedBillingAddress["name"] as? String, "Bobby Tables")
XCTAssertEqual(encodedBillingAddress["line_1"] as? String, "123 Fake St")
XCTAssertNil(encodedBillingAddress["line_2"])
XCTAssertEqual(encodedBillingAddress["locality"] as? String, "Utopia")
XCTAssertEqual(encodedBillingAddress["administrative_area"] as? String, "CA")
XCTAssertEqual(encodedBillingAddress["postal_code"] as? String, "90210")
XCTAssertEqual(encodedBillingAddress["country_code"] as? String, "US")
}

func testBillingAddressEncodedAsParametersNonNilLine2() throws {
let billingAddress = BillingAddress(
name: "Bobby Tables",
line1: "123 Fake St",
line2: "",
city: "Utopia",
state: "CA",
postalCode: "90210",
countryCode: "US"
)
let encodedBillingAddress = try FinancialConnectionsAPIClient.encodeAsParameters(billingAddress)

XCTAssertEqual(encodedBillingAddress["name"] as? String, "Bobby Tables")
XCTAssertEqual(encodedBillingAddress["line_1"] as? String, "123 Fake St")
XCTAssertNil(encodedBillingAddress["line_2"])
XCTAssertEqual(encodedBillingAddress["locality"] as? String, "Utopia")
XCTAssertEqual(encodedBillingAddress["administrative_area"] as? String, "CA")
XCTAssertEqual(encodedBillingAddress["postal_code"] as? String, "90210")
XCTAssertEqual(encodedBillingAddress["country_code"] as? String, "US")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {

var address: PaymentSheet.Address {
PaymentSheet.Address(
city: addressElement?.city?.text,
country: addressElement?.selectedCountryCode,
line1: addressElement?.line1?.text,
line2: addressElement?.line2?.text,
postalCode: addressElement?.postalCode?.text,
state: addressElement?.state?.rawData
city: addressElement?.city?.text ?? defaultAddress?.city,
country: addressElement?.selectedCountryCode ?? defaultAddress?.country,
line1: addressElement?.line1?.text ?? defaultAddress?.line1,
line2: addressElement?.line2?.text ?? defaultAddress?.line2,
postalCode: addressElement?.postalCode?.text ?? defaultAddress?.postalCode,
state: addressElement?.state?.rawData ?? defaultAddress?.state
)
}

Expand All @@ -111,6 +111,18 @@ final class InstantDebitsPaymentMethodElement: ContainerElement {
return configuration.defaultBillingDetails.address
}

var billingAddress: BillingAddress {
BillingAddress(
name: name,
line1: address.line1,
line2: address.line2,
city: address.city,
state: address.state,
postalCode: address.postalCode,
countryCode: address.country
)
}

var enableCTA: Bool {
let nameValid: Bool = {
// If the name field isn't shown, we treat the name as valid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,22 @@ extension PaymentMethodFormViewController {
phoneNumber: instantDebitsFormElement?.phone ?? configuration.defaultBillingDetails.phone
)
let linkMode = elementsSession.linkSettings?.linkMode
let billingAddress: BillingAddress? = {
if configuration.billingDetailsCollectionConfiguration.address == .full {
return instantDebitsFormElement?.billingAddress
} else if configuration.billingDetailsCollectionConfiguration.name == .always {
return BillingAddress(name: instantDebitsFormElement?.name)
} else {
return nil
}
}()
return ElementsSessionContext(
amount: intent.amount,
currency: intent.currency,
prefillDetails: prefillDetails,
intentId: intentId,
linkMode: linkMode
linkMode: linkMode,
billingAddress: billingAddress
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1765,8 +1765,7 @@ class PaymentSheetFormFactoryTest: XCTestCase {
XCTAssertEqual(instantDebitsSection.defaultEmail, "[email protected]")
XCTAssertEqual(instantDebitsSection.phone, "+12345678900")
XCTAssertEqual(instantDebitsSection.defaultPhone, "+12345678900")
// Unlike the other fields, the `address` will not fallback to the default.
XCTAssertEqual(instantDebitsSection.address, PaymentSheet.Address())
XCTAssertEqual(instantDebitsSection.address, defaultAddress)
XCTAssertEqual(instantDebitsSection.defaultAddress, defaultAddress)
}

Expand Down
Loading