Closed jbloodwhatnot closed 1 year ago
Hello @jbloodwhatnot -
There should not be a case where mostRecentPaymentMethod
returns nil for both the result and error (source code called from the method you are calling here) so this behavior is certainly unexpected.
You mention that this is only happening in production and you have been able to reproduce in the sandbox environment, is my understanding correct? Are you able to send over a video or screenshot of what is meant by "getting an error when paying for goods"? Can you also provide the steps and environment used to reproduce the error locally?
I look forward to hearing back and troubleshooting further.
Hello @jbloodwhatnot -
There should not be a case where
mostRecentPaymentMethod
returns nil for both the result and error (source code called from the method you are calling here) so this behavior is certainly unexpected.You mention that this is only happening in production and you have been able to reproduce in the sandbox environment, is my understanding correct? Are you able to send over a video or screenshot of what is meant by "getting an error when paying for goods"? Can you also provide the steps and environment used to reproduce the error locally?
I look forward to hearing back and troubleshooting further.
Sorry we can only reproduce this in production. We cannot reproduce in sandbox environment. It seems to be specifically Apply Pay cards that we are having issue with in production. All reports from our users are using Apple Pay cards. It works fine in sandbox environment using the simulated apple pay cards. But in production we can reproduce it in our app.
Steps to reproduce are.
mostRecentPaymentMethod
to fetch the users most recent card. So that they can pay for something in the app. But the issue is the method is returning null in both fields sadly.I can't really give a local project to reproduce as its only happening on production for whatever reason.
š Hi @jbloodwhatnot - thanks for the additional detail.
Can you confirm if you are talking about the Card
(red) OR Apple Pay
(blue) payment method type, as shown in the screenshot below?
It sounds like you are talking about the Card
(red) selection option above, and then entering an Apple Card into the Card Number
text field?
Hi @scannillo,
So in our case on the app the actual payment method is being setup on the server using the Braintree Python SDK. But the card that is being added is a card from the users Apple Pay wallet.
That cards being added via the backend are being stored correctly as I can see the cards in the Braintree Dashboard in the vault section on my Account.But for some reason the SDK is not returning these cards and the odd scenario I described of the fields being both null is happening.
So on the app the flow is. The app uses PKPaymentAuthorizationViewController as part of the Apply Pay api's to authorize a card that is stored in users apple Pay wallet. Using the card from this authorization we send the card to be stored to our backend server which is using the Braintree Python SDK to store the card. We can see the card in the dashboard. But SDK is not returning these Apple Pay Wallet cards for some reason.
š Hi again @jbloodwhatnot - thanks for providing more details, however we still need more information in order to reproduce.
Can you provide code snippets for how you're vaulting the Apple Pay payment method using the Braintree Python SDK? If you can also provide relevant client-side code snippets for how you're tokenizing Apple Pay, that would be helpful too.
š Hi again @jbloodwhatnot - thanks for providing more details, however we still need more information in order to reproduce.
Can you provide code snippets for how you're vaulting the Apple Pay payment method using the Braintree Python SDK? If you can also provide relevant client-side code snippets for how you're tokenizing Apple Pay, that would be helpful too.
Hi there is a decent amount of code here. But tried to distill it down to focus on the flow on iOS side of the Apply Pay Card being added and also on the python side.
public class PaymentManager: NSObject {
fileprivate static let shared: PaymentManager = PaymentManager()
var completionHandler: ((PaymentSelectionResult) -> Void)?
private override init() {}
static func braintreeClient() async throws -> BTAPIClient {
// fetchPaymentToken() is calling to our backend to fetch e.g as described here. https://developer.paypal.com/braintree/docs/start/hello-server#generate-a-client-token
let token = try await fetchPaymentToken()
guard let client = BTAPIClient(authorization: token) else {
AppLogger.log(.error, message: "BRAINTREE API AUTH ERROR", error: PaymentError.braintreeNoAPIClient)
throw PaymentError.braintreeNoAPIClient
}
return client
}
// set up PKPaymentRequest of 0.00 for authorization
func newPaymentRequest() async throws -> PKPaymentRequest {
let client = try await Self.braintreeClient()
let applePayClient = BTApplePayClient(apiClient: client)
return try await withCheckedThrowingContinuation { continuation in
applePayClient.paymentRequest { request, error in
guard error == nil else {
AppLogger.log(.error, message: "BRAINTREE APPLEPAY ERROR", error: error!)
continuation.resume(throwing: error!)
return
}
guard let request = request else {
AppLogger.log(.error, message: "BRAINTREE APPLEPAY ERROR", error: PaymentError.braintreeNoResult)
continuation.resume(throwing: PaymentError.braintreeNoResult)
return
}
request.requiredBillingContactFields = [.name, .postalAddress]
request.merchantCapabilities = .capability3DS
continuation.resume(returning: request)
}
}
}
static func paymentRequestForAuthorization() async throws -> PKPaymentRequest {
let request = try await newPaymentRequest()
request.paymentSummaryItems = [
.init(label: "Payment Authorization", amount: 0.00),
]
return request
}
// Setup apple pay card
func setupApplePay(completion: ((PaymentSelectionResult) -> Void)? = nil) {
Task {
do {
if let completion = completion {
PaymentManager.shared.completionHandler = completion
}
let req = try await PaymentManager.paymentRequestForAuthorization()
await presentApplePaySetupAuthorization(req)
} catch {
displayError(error: error)
}
}
}
@MainActor func presentApplePaySetupAuthorization(_ request: PKPaymentRequest) {
guard let vc = PKPaymentAuthorizationViewController(paymentRequest: request) else { return }
vc.delegate = PaymentManager.shared
present(vc, animated: true, completion: nil)
}
// handle the sucess result from presenting the PKPaymentAuthorizationViewController
public func paymentAuthorizationViewController(_ controller: PKPaymentAuthorizationViewController, didAuthorizePayment payment: PKPayment) async -> PKPaymentAuthorizationResult {
do {
let result = try await handleApplePayPayment(payment)
// calling our backend here to save the card server side e.g using python sdk code is included down lower
try await PaymentService.savePaymentMethodString(result.encodedPaymentMethod)
handleAuthorizedPaymentResult(.success(result.paymentModel))
return .init(status: .success, errors: [])
} catch {
AppLogger.log(error: error)
handleAuthorizedPaymentResult(.failure(error))
return .init(status: .failure, errors: [error])
}
}
private func handleAuthorizedPaymentResult(_ result: Result<BINPaymentModel, Error>) {
if let completionHandler = completionHandler {
if case let .failure(error) = result {
completionHandler(.error(error))
} else {
completionHandler(.updated)
}
}
completionHandler = nil
}
func handleApplePayPayment(_ payment: PKPayment) async throws -> ApplePayResult {
let client = try await Self.braintreeClient()
let applePayClient = BTApplePayClient(apiClient: client)
let nonce = try await applePayClient.tokenizeApplePay(payment)
let cd = CardDescription(
nonce: nonce.nonce,
type: "\(BTDropInPaymentMethodType.applePay.rawValue)",
description: "Apple Pay"
)
//cd.jsonString just convertering CardDescription object into json representation
guard let token = cd.jsonString else {
throw PaymentError.noData
}
let paymentModel = BINPaymentModel(cardToken: token, last4: "", gateway: .braintree, isApplePay: true)
return ApplePayResult(paymentModel: paymentModel, encodedPaymentMethod: token)
}
}
public struct CardDescription: Codable {
let nonce: String?
let type: String
let description: String
var deviceData: String = ""
public init(nonce: String?, type: String, description: String, deviceData: String = "") {
self.nonce = nonce
self.type = type
self.description = description
self.deviceData = deviceData
}
}
payment.py
// Python code for storing card from graphql api call
@mutation.field("createPaymentMethod")
@convert_kwargs_to_snake_case
def create_payment_method_mutation(obj, info, **kwargs):
user = info.context.viewer_context.get_user()
pd = kwargs.get("payment_data", "{}")
payment_data = json.loads(pd) if not isinstance(pd, dict) else pd
pm = BraintreePaymentMethodCreator(db_session=db.session, user=user)
card = pm.create(payment_data, billing_address=None, default=True)
return {"payment_method": card}
class BraintreePaymentMethodCreator(BaseUsPaymentCommand):
def create(self, payment_data: dict, **kwargs):
from whatnot.commands.payment import BrainTreeCardStorage
bt = BrainTree(self.user)
nonce = payment_data.get("nonce")
device_data = payment_data.get("payment_data")
default = kwargs.get("default", False)
billing_address = kwargs.get("billing_address") or self.user.billing_address
payment_method = bt.get_or_create_payment(
nonce,
device_data,
billing_address=billing_address,
)
// card is stored in our own database but ommited for brevity
card = storeInDb(payment_method)
return card
class BrainTree:
def __init__(self, user=None):
from flask import current_app
self.session = db.session
self.user = user
production = current_app.config.get("BRAINTREE_PRODUCTION_ENABLED")
self.gateway = braintree.BraintreeGateway(
braintree.Configuration(
braintree.Environment.Sandbox
if not production
else braintree.Environment.Production,
merchant_id=current_app.config.get("BRAINTREE_MERCHANT_ID"),
public_key=current_app.config.get("BRAINTREE_PUBLIC_KEY"),
private_key=current_app.config.get("BRAINTREE_PRIVATE_KEY"),
)
)
def get_or_create_payment(
self, nonce, device_data=None, default=False, billing_address=None
):
account_id = (
self.user.braintree_account_id
if self.user.braintree_account_id
else self.get_customer().id
)
parameters = {
"customer_id": str(account_id),
"payment_method_nonce": nonce,
"device_data": device_data,
"options": {
"make_default": default,
"verify_card": True,
},
}
if billing_address is not None:
parameters["billing_address"] = {
"first_name": billing_address.first_name,
"last_name": billing_address.last_name,
"street_address": billing_address.line1,
"extended_address": billing_address.line2,
"region": billing_address.state,
"postal_code": billing_address.postal_code,
"country_code_alpha2": billing_address.country_code,
}
result = self.gateway.payment_method.create(parameters)
if result.is_success:
return result.payment_method
if result.credit_card_verification is not None:
raise Exception(result.credit_card_verification.status)
else:
raise Exception(
result.message
or "Failed to validate Credit Card. Please try a different one"
)
Screenshot of the added cards when looking at my account in the braintree dashboard. Have redacted most of the fields. But the cards appear fine here. They just don't return in the SDK.
Howdy, Any updates on this or any luck in reproducing ? Or more info I can provide ?
š Hi @jbloodwhatnot - so, to confirm, you are calling the Python SDKs create
method with the nonce
and customerID
for an Apple Pay payment?
Are you having this issue with any other payment methods (ex: Card, PayPal, Venmo)?
If you want to check out the branch gh-issue-383
, I pushed up this commit, which might resolve the issue you were seeing of a (nil, nil)
completion. Can you let me know if that branch solves that part of the issue?
š Hi @jbloodwhatnot - so, to confirm, you are calling the Python SDKs
create
method with thenonce
andcustomerID
for an Apple Pay payment?Are you having this issue with any other payment methods (ex: Card, PayPal, Venmo)?
Hi, Yes this is correct. Also seems to just be specific to apple pay for us as far as I can tell. Other methods seem fine.
If you want to check out the branch
gh-issue-383
, I pushed up this commit, which might resolve the issue you were seeing of a(nil, nil)
completion. Can you let me know if that branch solves that part of the issue?
Just back from holidays. Will give this a test in the new couple of days and let you know if it resolves. Thanks
If you want to check out the branch
gh-issue-383
, I pushed up this commit, which might resolve the issue you were seeing of a(nil, nil)
completion. Can you let me know if that branch solves that part of the issue?
Tested this branch out now. I am now correctly getting the error field populated with your change that you made. The error returned is "No recent payment methods found." But still not clear why their are no recent payment methods. As here is my vault for my customer id. Have blurred out most fields to be on safe side. (Note I have multiple apple pay cards as its me adding my same card multiple times while testing this issue out)
Hello @jbloodwhatnot -
The fix for the (nil, nil)
completion was just released in version 9.8.0 of the Drop-in SDK.
Regarding the error you are seeing, we will likely need our internal team to look into more specifics of your account to troubleshoot further. Would you mind reaching out to our Support team via the help form and provide them with details of your gateway account for them to investigate further? Feel free to link them to this Github issues as well as we can provide them with details internally to help troubleshoot further.
Hi @jbloodwhatnot - we were able to replicate the behavior you're seeing with our Support team. We will keep you posted.
š Hi @jbloodwhatnot - so working together with Support, we learned a few things.
The behavior you're seeing is expected. ~3 months ago there was a change made, that essentially acted as a bug-fix to this functionality since it was not working as expected prior.
In our docs, we state that vaulting Apple Pay cards is only for recurring billing transactions and merchant-initiated transactions, not customer-initiated transactions. Apparently the documentation was always accurate, but the backend APIs were allowing non-recurring billing/customer-initiated Apple Pay transactions to vault. So a "fix" was made, but unfortunately nothing was communicated out to the docs since the documentation was accurate.
For more information details on how you can utilize recurring billing, please reach back out to Braintree Support.
Integration Details (please complete the following information):
Describe the bug We are getting reports of users who have added apple pay cards that they are getting an error when paying for goods. After investigating the number of user reports we have verified that the users custom vault correctly has the apply pay cards added to their customer account. But the error is happening when we attempt to fetch the users most recent payment method. The response from the Braintree sdk callback has the error and result field as both as null. Which is causing the app to fail. Any idea what the issue could possibly be in order for both of these fields to return null.
Code snippet below:
What we are seeing is the callback is returning both res and error field as nil. (Which I would have assumed one of these fields would always return non nil ? Any idea of what the cause for this could be.
From our logs it seems like its users using Apply Pay cards that are effected. It has started happening for our users for us since 31st October. I was able to reproduce locally but its hard to debug the issue as the SDK is not really giving any hints as to what has gone wrong here. I checked the users who had this issue and all had the Apple Cards correctly added to their paypal vaults/custom accounts.
To Reproduce Steps to reproduce the behavior:
Expected behavior Description of what you expected to happen.
mostRecentPaymentMethod should return the users Apple Pay card.
Screenshots N/A