I have written code in .NET Standard 2.0 to make payments through Braintree. The code uses the Braintree 5.2.0 NuGet package. I intend to exclusively make 3D Secure payments when the code is used against a Braintree production account. I have written a integration test that creates a customer, creates a payment method for that customer, then makes a payment against that payment method using the Token that was generated.
The code to create a customer is:
public async Task<string> SeedCustomer(IBraintreeConfiguration braintreeConfiguration)
{
BraintreeGateway _braintreeGateway = new BraintreeGateway(
braintreeConfiguration.Environment,
braintreeConfiguration.MerchantId,
braintreeConfiguration.PublicKey,
braintreeConfiguration.PrivateKey
);
CustomerRequest request = new CustomerRequest
{
Email = BraintreeTestConstants.Email,
CustomFields = new Dictionary<string, string>
{
{"account_id", BraintreeTestConstants.AccountId}
}
};
string braintreeCustomerId =
(await _braintreeGateway.Customer.CreateAsync(request)).Target.Id;
return braintreeCustomerId;
}
The code to create a payment method is:
PaymentMethodRequest paymentMethodRequest = new PaymentMethodRequest
{
PaymentMethodNonce = nonce,
CustomerId = customerId,
BillingAddress = new PaymentMethodAddressRequest
{
FirstName = BraintreeTestConstants.BillingName,
Locality = BraintreeTestConstants.City,
Company = BraintreeTestConstants.CompanyName,
CountryCodeAlpha2 = BraintreeTestConstants.Country,
ExtendedAddress = BraintreeTestConstants.ExtendedAddress,
Region = BraintreeTestConstants.State,
StreetAddress = BraintreeTestConstants.StreetAddress,
PostalCode = BraintreeTestConstants.Zip,
},
Options = new PaymentMethodOptionsRequest
{
VerifyCard = true
}
};
Result<PaymentMethod> result =
await _braintreeGateway.PaymentMethod.CreateAsync(paymentMethodRequest);
return _mapper.Map<AddPaymentMethodResultModel>((CreditCard)result.Target);
CreditCard.Token is mapped to AddPaymentMethodResultModel.CardId.
The code to make a payment is:
bool useThreeDSecure = true;
TransactionRequest transactionRequest = new TransactionRequest
{
Amount = Amount,
PaymentMethodToken = CardId,
MerchantAccountId = string.IsNullOrWhiteSpace(MerchantAccountId) ? null : MerchantAccountId,
TransactionSource = string.IsNullOrWhiteSpace(TransactionSource) ? null : TransactionSource,
Options = new TransactionOptionsRequest
{
SubmitForSettlement = true,
ThreeDSecure = new TransactionOptionsThreeDSecureRequest
{
Required = useThreeDSecure
}
}
};
if (Address != null)
{
transactionRequest.BillingAddress = new AddressRequest
{
Company = Address.CompanyName,
FirstName = Address.BillingName,
StreetAddress = Address.StreetAddress,
ExtendedAddress = Address.ExtendedAddress,
Locality = Address.City,
Region = Address.State,
PostalCode = Address.Zip,
CountryCodeAlpha2 = Address.Country
};
}
Result<Transaction> result =
await _braintreeGateway.Transaction.SaleAsync(transactionRequest);
When I execute the test against a Braintree sandbox environment with TransactionRequest.Options.ThreeDSecure.Required set to false, a payment is successful.
When I execute the test against a Braintree sandbox environment TransactionRequest.Options.ThreeDSecure.Required set to true, a payment fails with a Result.Message of Gateway Rejected: three_d_secure.
I was wondering whether it is possible to make a successful payment through a Braintree sandbox environment with TransactionRequest.Options.ThreeDSecure.Required set to true. I have tried unsuccessfully using a payment method created through the NuGet package with a PaymentMethodNonce of fake-three-d-secure-visa-full-authentication-nonce.
I would like to prove that a 3D Secure payment can be made through a Braintree sandbox environment to have confidence that the code will work against a Braintree production environment.
There is a client-side piece that must be implemented to validate a vaulted card before it can be used to make a 3D Secure payment.
There are two approaches that can be taken to achieve this: drop-in UI or hosted fields. Drop-in UI provides a form with some styling, while hosted field provides more control over the appearance of your form. I did not require the fields to be visible, as I could manage the data through JavaScript alone, so I used hosted fields.
First, one must generate a client token, as well as a nonce and bin representing the vaulted card on the server-side. The code for achieving this was:
public async Task<string> GetBraintreeClientToken()
{
string clientToken =
await _braintreeChargeService.GetClientToken();
return clientToken;
}
public async Task<BraintreeValidationDataModel>
GetValidationDataForVaultedToken(string paymentMethodToken)
{
Result<PaymentMethodNonce> result = await
_braintreeGateway.PaymentMethodNonce.CreateAsync(paymentMethodToken);
PaymentMethodNonce paymentMethodNonce =
result.Target;
BraintreeValidationDataModel validationData = new
BraintreeValidationDataModel
{
Nonce = paymentMethodNonce.Nonce,
Bin = paymentMethodNonce.Details.Bin
};
return validationData;
}
Next, one must use this information on the client-side to generate a nonce that is valid for a 3D Secure payment. To instantiate the Braintree components:
function setupComponents(clientToken) {
return Promise.all([
braintree.threeDSecure.create({
authorization: clientToken,
version: 2
}),
braintree.dataCollector.create({
authorization: clientToken
})
]);
}
To generate a 3D Secure nonce:
return setupComponents(threeDsValidation.clientToken).then(function (result) {
var threeDSecure =
result[0];
if (threeDSecure === null ||
threeDSecure === undefined) {
return null;
}
var dataCollector =
result[1];
if (dataCollector === null ||
dataCollector === undefined ||
dataCollector.deviceData == null) {
return null;
}
threeDsValidationNew.deviceData =
dataCollector.deviceData;
var threeDSecureParameters =
getThreeDsSecureParameters(threeDsValidationNew);
return threeDSecure.verifyCard(threeDSecureParameters);
})
.then(function (verifyCardResult) {
if (verifyCardResult === null ||
verifyCardResult === undefined ||
verifyCardResult.nonce == null) {
return null;
}
threeDsValidationNew.nonceSecure =
verifyCardResult.nonce;
return threeDsValidationNew;
});
Where getThreeDsSecureParameters is:
function getThreeDsSecureParameters(threeDsValidation) {
return {
amount: threeDsValidation.amount.toString(),
nonce: threeDsValidation.paymentMethodNonce,
bin: threeDsValidation.bin,
email: threeDsValidation.braintreeCharge.email,
billingAddress: {
givenName: threeDsValidation.braintreeCharge.billingName,
phoneNumber: threeDsValidation.braintreeCharge.phone,
streetAddress: threeDsValidation.braintreeCharge.streetAddress,
extendedAddress: threeDsValidation.braintreeCharge.extendedAddress,
locality: threeDsValidation.braintreeCharge.city,
region: threeDsValidation.braintreeCharge.state,
postalCode: threeDsValidation.braintreeCharge.zip,
countryCodeAlpha2: threeDsValidation.braintreeCharge.country
},
additionalInformation: {
shippingGivenName: threeDsValidation.braintreeCharge.shippingName,
shippingPhone: threeDsValidation.braintreeCharge.shippingPhone,
shippingAddress: {
streetAddress: threeDsValidation.braintreeCharge.shippingStreetAddress,
extendedAddress: threeDsValidation.braintreeCharge.shippingExtendedAddress,
locality: threeDsValidation.braintreeCharge.shippingCity,
region: threeDsValidation.braintreeCharge.shippingState,
postalCode: threeDsValidation.braintreeCharge.shippingZip,
countryCodeAlpha2: threeDsValidation.braintreeCharge.shippingCountry
}
},
onLookupComplete: function (data, next) {
next();
}
};
}
The secure nonce returned by verifyCard can then be used as the PaymentMethodNonce on the server side to complete a 3D Secure transaction. An additional field of DeviceData can be added to the TransactionRequest on the server-side, which is equal to the deviceData field of the dataCollector object that was created in the setupComponents method.
TransactionRequest transactionRequest = new TransactionRequest
{
Amount = Amount,
PaymentMethodNonce = PaymentMethodNonce,
DeviceData = string.IsNullOrWhiteSpace(DeviceData) ? null : DeviceData,
MerchantAccountId = string.IsNullOrWhiteSpace(MerchantAccountId) ? null : MerchantAccountId,
TransactionSource = string.IsNullOrWhiteSpace(TransactionSource) ? null : TransactionSource,
CustomerId = string.IsNullOrWhiteSpace(CustomerId) ? null : CustomerId,
Options = new TransactionOptionsRequest
{
SubmitForSettlement = true,
ThreeDSecure = new TransactionOptionsThreeDSecureRequest
{
Required = chargeOptions.UseThreeDSecure
}
}
};
if (Address != null)
{
transactionRequest.BillingAddress = new AddressRequest
{
Company = string.IsNullOrWhiteSpace(Address.CompanyName) ? null : Address.CompanyName,
FirstName = string.IsNullOrWhiteSpace(Address.BillingName) ? null : Address.BillingName,
StreetAddress = string.IsNullOrWhiteSpace(Address.StreetAddress) ? null : Address.StreetAddress,
ExtendedAddress = string.IsNullOrWhiteSpace(Address.ExtendedAddress) ? null : Address.ExtendedAddress,
Locality = string.IsNullOrWhiteSpace(Address.City) ? null : Address.City,
Region = string.IsNullOrWhiteSpace(Address.State) ? null : Address.State,
PostalCode = string.IsNullOrWhiteSpace(Address.Zip) ? null : Address.Zip,
CountryCodeAlpha2 = string.IsNullOrWhiteSpace(Address.Country) ? null : Address.Country
};
}
IBraintreeChargeReturnModel result = await Pay(transactionRequest,
braintreeGateway,
chargeOptions.UseThreeDSecure);
Be aware that the client-side code may display a window asking for the customer to complete additional validation when used with a production Braintree account.