Skip to content

Latest commit

 

History

History
1083 lines (834 loc) · 33.2 KB

v4.9.0+_MIGRATION_GUIDE.md

File metadata and controls

1083 lines (834 loc) · 33.2 KB

Braintree Android v4.9.0+ Migration Guide

See the CHANGELOG for a complete list of changes. This migration guide outlines the basics for updating your Braintree integration from v3 to v4.

Documentation for v4 is available at https://developer.paypal.com/braintree/docs/start/hello-client/android/v4.

Table of Contents

  1. Gradle
  2. Browser Switch
  3. BraintreeFragment
  4. Client Token Provider
  5. Event Handling
  6. Builder Pattern
  7. Fragment Architecture
  8. American Express
  9. Card
  10. PayPal Data Collector
  11. Local Payment
  12. Google Pay
  13. PayPal
  14. Samsung Pay
  15. Visa Checkout
  16. Union Pay
  17. Venmo
  18. 3D Secure
  19. Integrating Multiple Payment Methods
  20. Manual Browser Switching for Browser-Based Flows

Gradle

The features of the Braintree SDK are now organized into modules and can each be imported as dependencies in your build.gradle file. You must remove the com.braintreepayments.api:braintree:3.x.x dependency when migrating to v4.

The examples below show the required dependencies for each feature.

Browser Switch

In v3, com.braintreepayments.api.BraintreeBrowserSwitchActivity was the designated deep link destination activity maintained by the Braintree SDK. In v4, we've removed BraintreeBrowserSwitchActivity to give apps more control over their deep link configuration.

In the AndroidManifest.xml, migrate the intent-filter from your v3 integration into an activity you own:

Note: android:exported is required if your app compile SDK version is API 31 (Android 12) or later.

<activity android:name="com.company.app.MyPaymentsActivity"
    android:exported="true">
    ...
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <data android:scheme="${applicationId}.braintree"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
    </intent-filter>
</activity>

Additionally, apps that use both Drop-in and BraintreeClient should specify a custom url scheme, since DropInActivity already uses the ${applicationId}.braintree url intent filter.

If your app has multiple browser switch targets, you can specify multiple intent filters and use the BraintreeClient constructor that allows you to specify a customUrlScheme:

<activity android:name="com.company.app.MyPaymentsActivity1"
    android:exported="true">
    ...
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <data android:scheme="custom-url-scheme-1"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
    </intent-filter>
</activity>

<activity android:name="com.company.app.MyPaymentsActivity2"
    android:exported="true">
    ...
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <data android:scheme="custom-url-scheme-2"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
    </intent-filter>
</activity>

Then when constructing your BraintreeClient make sure to pass the appropriate custom url scheme for each deep link target Activity:

// MyPaymentsActivity1.java
BraintreeClient braintreeClient =
    new BraintreeClient(this, "TOKENIZATION_KEY_OR_CLIENT_TOKEN", "custom-url-scheme-1");
// MyPaymentsActivity2.java
BraintreeClient braintreeClient =
    new BraintreeClient(this, "TOKENIZATION_KEY_OR_CLIENT_TOKEN", "custom-url-scheme-2");

BraintreeFragment

In v4, we decoupled the Braintree SDK from Android to offer more integration flexibility. BraintreeFragment has been replaced by a Client for each respective payment feature. See the below payment method sections for examples of instantiating and using the feature clients.

Client Token Provider

When creating a BraintreeClient, you can provide a tokenization key, client token, or a ClientTokenProvider. When given a ClientTokenProvider, the SDK will fetch a client token on your behalf when it is needed. This makes it possible to construct a BraintreeClient instance using client token authorization in onCreate.

Assuming you have a server that supports GET https://www.my-api.com/client_token and receives the following json response:

{
  "value": "<CLIENT_TOKEN>"
}

Here is an example ClientTokenProvider implementation using Retrofit 2.x:

// ClientToken.java
public class ClientToken {

  @SerializedName
  private String value;

  public String getValue() {
    return value;
  }
}

// Api.java
public interface Api {

  @GET("/client_token")
  Call<ClientToken> getClientToken();
}

// ExampleClientTokenProvider.java
class ExampleClientTokenProvider implements ClientTokenProvider {

  private static final Retrofit.Builder builder = new Retrofit.Builder()
    .baseUrl("https://my-api.com")
    .addConverterFactory(GsonConverterFactory.create());

  private static final OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

  public static Api createService() {
    builder.client(httpClient.build());
    Retrofit retrofit = builder.build();
    return retrofit.create(Api.class);
  }

  void getClientToken(@NonNull ClientTokenCallback callback) { 
    Call<ClientToken> call = createService().getClientToken();
    call.enqueue(new Callback<ClientToken>() {
      @Override
      public void onResponse(Call<ClientToken> call, Response<ClientToken> response) { 
        callback.onSuccess(response.body().getValue());
      }

      @Override
      public void onFailure(Call<ClientToken> call, Throwable t) {
        callback.onFailure(new Exception(t));
      }
    });
  }
}

Then in an Activity or Fragment, create an instance of BraintreeClient:

// ExampleActivity.java
public class ExampleActivity extends AppCompatActivity {

  private BraintreeClient braintreeClient;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, new ExampleClientTokenProvider());
  }
}

Event Handling

In v3, there were several interfaces that would be called when events occurred: PaymentMethodNonceCreatedListener, ConfigurationListener, BraintreeCancelListener, and BraintreeErrorListener. In v4, these listeners have been replaced by a callback pattern for flows that do not require an app or browser switch, and new listeners for payment flows that do require an app or browser switch.

Handling PaymentMethodNonce Results

For payment methods that do not require leaving the application, the result will be returned via the callback passed into the tokenization method. For example, using the CardClient:

cardClient.tokenize(card, (cardNonce, error) -> {
  // send cardNonce.getString() to your server or handle error
});

For payment methods that require a browser switch or app switch, the result will be returned via the payment method listener (ex: PayPalListener). For example, using the PayPalClient:

public class MerchantActivity extends AppCompatActivity implements PayPalListener {
    ...

    @Override
    public void onPayPalSuccess(PayPalAccountNonce payPalAccountNonce) {
    // send nonce to server and create a transaction
    }

    @Override
    public void onPayPalFailure(Exception error) {
    // handle error
    }

}

Full implementation examples can be found in the payment method feature sections below.

Handling Cancellation

When the customer cancels out of a payment flow, a UserCanceledException will be returned in the callback of the invoked method.

Handling Errors

Errors will be returned to the callback of the invoked method for methods that do no invoke an app or browser switch or to the payment method listener for methods that do invoke an app or browser switch.

Fetching Configuration

If you need to fetch configuration, use BraintreeClient#getConfiguration().

Previously, this was done via BraintreeFragment:

braintreeFragment.addListener(new ConfigurationListener() {

  void onConfigurationFeetched(Configuration configuration) {

  }
});

Builder Pattern

The builder pattern has been removed in v4 to allow for consistent object creation across Java and Kotlin. Classes have been renamed without the Builder postfix, method chaining has been removed, and setters have been renamed with the set prefix.

For example, CardBuilder in v3 becomes Card in v4:

Card card = new Card();
card.setNumber("4111111111111111");
card.setExpirationDate("12/2022");

Builder classes that have been renamed:

  • CardBuilder is now Card
  • UnionPayCardBuilder is now UnionPayCard

Fetching Payment Methods

In v3, the PaymentMethod class could be used to retrieve a current list of PaymentMethodNonce objects for the current customer. In v4, that functionality will be moved to drop-in.

Fragment Architecture

The code snippets for the example payment method flows show how to integrate each payment method into an Activity. These payment methods can also be integrated into a Fragment.

To integrate via Fragment, update your fragment to implement the applicable <PaymentMethod>Listener. Construct the BraintreeClient and <PaymentMethod>Client within the fragment, and set the fragment as the listener on the <PaymentMethod>Client.

There are certain methods that require a FragmentActivity as a parameter. This is necessary for handling app or browser switches within Android. To invoke these methods from within a fragment, pass requireActivity() as the parameter.

American Express

The American Express feature is now supported by implementing the following dependencies:

dependencies {
  implementation 'com.braintreepayments.api:american-express:4.47.0'
  implementation 'com.braintreepayments.api:card:4.47.0'
}

To use the feature, instantiate an AmericanExpressClient:

package com.my.app;

public class AmericanExpressActivity extends AppCompatActivity {

  private BraintreeClient braintreeClient;
  private AmericanExpressClient americanExpressClient;
  private CardClient cardClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    americanExpressClient = new AmericanExpressClient(braintreeClient);

    // you will also need a card client for tokenization in this example
    cardClient = new CardClient(braintreeClient);
  }

  private void tokenizeCard() {
    Card card = new Card();
    card.setNumber("378282246310005");
    card.setExpirationDate("12/2022");

    cardClient.tokenize(card, (cardNonce, error) -> {
      if (cardNonce != null) {
        getAmexRewardsBalance(cardNonce);
      } else {
        // handle error
      }
    });
  }

  private void getAmexRewardsBalance(CardNonce cardNonce) {
    String nonceString = cardNonce.getString();
    americanExpressClient.getRewardsBalance(nonceString, "USD", (rewardsBalance, error) -> {
      if (rewardsBalance != null) {
        // display rewards amount to user
        String rewardsAmount = rewardsBalance.getRewardsAmount();
      } else {
        // handle error
      }
    });
  }
}

Card

The Card feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:card:4.47.0'
}

To use the feature, instantiate a CardClient:

package com.my.app;

public class CardActivity extends AppCompatActivity {

  private BraintreeClient braintreeClient;
  private CardClient cardClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    cardClient = new CardClient(braintreeClient);
  }

  private void tokenizeCard() {
    Card card = new Card();
    card.setNumber("4111111111111111");
    card.setExpirationDate("12/2022");

    cardClient.tokenize(card, (cardNonce, error) -> {
      if (cardNonce != null) {
        // send this nonce to your server
        String nonce = cardNonce.getString();
      } else {
        // handle error
      }
    });
  }
}

Card Validation

The validate property on Card has been renamed to shouldValidate.

PayPal Data Collector

The PayPal Data Collector feature is now supported in the following dependency:

dependencies {
  implementation 'com.braintreepayments.api:paypal-data-collector:4.47.0'
}

To use the feature, instantiate a PayPalDataCollector:

package com.my.app;

public class PaymentsActivity extends AppCompatActivity {

  private BraintreeClient braintreeClient;
  private PayPalDataCollector dataCollector;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    dataCollector = new PayPalDataCollector(braintreeClient);
  }
  
  private void collectDeviceData() {
    dataCollector.collectDeviceData(this, (deviceData, error) -> {
      // send deviceData to your server
    });
  }
}

Local Payment

The Local Payment feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:local-payment:4.47.0'
}

To use the feature, instantiate a LocalPaymentClient:

package com.my.app;

public class LocalPaymentActivity extends AppCompatActivity implements LocalPaymentListener {

  private BraintreeClient braintreeClient;
  private LocalPaymentClient localPaymentClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    localPaymentClient = new LocalPaymentClient(this, braintreeClient);
    localPaymentClient.setListener(this);
  }

  @Override
  protected void onNewIntent(Intent newIntent) {
    super.onNewIntent(newIntent);
    // required if your activity's launch mode is "singleTop", "singleTask", or "singleInstance"
    setIntent(newIntent);
  }

  private void startLocalPayment() {
    PostalAddress address = new PostalAddress();
    address.setStreetAddress("836486 of 22321 Park Lake");
    address.setCountryCodeAlpha2("NL");
    address.setLocality("Den Haag");
    address.setPostalCode("2585 GJ");

    LocalPaymentRequest request = new LocalPaymentRequest();
    request.setPaymentType("ideal");
    request.setAmount("1.01");
    request.setAddress(address);
    request.setPhone("639847934");
    request.setEmail("[email protected]");
    request.setGivenName("Jon");
    request.setSurname("Doe");
    request.setShippingAddressRequired(true);
    request.setCurrencyCode("EUR");
     
    localPaymentClient.startPayment(request, (localPaymentTransaction, error) -> {
      if (localPaymentTransaction != null) {
        // do any pre-processing transaction.getPaymentId()
        localPaymentClient.approvePayment(MyLocalPaymentActivity.this, transaction);
      } else {
        // handle error
      }
    });
  }

  @Override
  public void onLocalPaymentSuccess(@NonNull LocalPaymentNonce localPaymentNonce) {
      // send nonce to server
  }

  @Override
  public void onLocalPaymentFailure(@NonNull Exception error) {
      // handle error
  }
}

Google Pay

The Google Pay feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:google-pay:4.47.0'
}

Note: The following wallet enabled metadata tag is now included by the SDK and is no longer required in your AndroidManifest.xml:

<meta-data android:name="com.google.android.gms.wallet.api.enabled" android:value="true"/>

To use the feature, instantiate an GooglePayClient:

package com.my.app;

public class GooglePayActivity extends AppCompatActivity implements GooglePayListener {

  private BraintreeClient braintreeClient;
  private GooglePayClient googlePayClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    googlePayClient = new GooglePayClient(this, braintreeClient);
    googlePayClient.setListener(this);
  }

  private void checkIfGooglePayIsAvailable() {
    googlePayClient.isReadyToPay(this, (isReadyToPay, error) -> {
      if (isReadyToPay) {
        // Google Pay is available
      } else {
        // handle error
      }
    });
  }

  private void makeGooglePayRequest() {
    GooglePayRequest googlePayRequest = new GooglePayRequest();
    googlePayRequest.setTransactionInfo(TransactionInfo.newBuilder()
      .setTotalPrice("1.00")
      .setTotalPriceStatus(WalletConstants.TOTAL_PRICE_STATUS_FINAL)
      .setCurrencyCode("USD")
      .build());
    googlePayRequest.setBillingAddressRequired(true);
    googlePayRequest.setGoogleMerchantId("merchant-id-from-google");

    googlePayClient.requestPayment(this, googlePayRequest);
  }

  @Override
  public void onGooglePaySuccess(@NonNull PaymentMethodNonce paymentMethodNonce) {
      // send nonce to server
  }

  @Override
  public void onGooglePayFailure(@NonNull Exception error) {
      // handle error
  }
}

PayPal

The PayPal feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:paypal:4.47.0'
}

To use the feature, instantiate a PayPalClient:

package com.my.app;

public class PayPalActivity extends AppCompatActivity implements PayPalListener {

  private BraintreeClient braintreeClient;
  private PayPalClient payPalClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    payPalClient = new PayPalClient(this, braintreeClient);
    payPalClient.setListener(this);
  }

  @Override
  protected void onNewIntent(Intent newIntent) {
    super.onNewIntent(newIntent);
    // required if your activity's launch mode is "singleTop", "singleTask", or "singleInstance"
    setIntent(newIntent);
  }

  private void myTokenizePayPalAccountWithCheckoutMethod() {
    PayPalCheckoutRequest request = new PayPalCheckoutRequest("1.00");
    request.setCurrencyCode("USD");
    request.setIntent(PayPalPaymentIntent.AUTHORIZE);

    payPalClient.tokenizePayPalAccount(this, request);
  }

  private void myTokenizePayPalAccountWithVaultMethod() {
    PayPalVaultRequest request = new PayPalVaultRequest();
    request.setBillingAgreementDescription("Your agreement description");

    payPalClient.tokenizePayPalAccount(this, request);
  }

  @Override
  public void onPayPalSuccess(@NonNull PayPalAccountNonce payPalAccountNonce) {
      // send nonce to server
  }

  @Override
  public void onPayPalFailure(@NonNull Exception error) {
      // handle error
  }
}

PayPal Request

v4 introduces two subclasses of PayPalRequest:

  • PayPalCheckoutRequest, for checkout flows
  • PayPalVaultRequest, for vault flows

The setters on the request classes have been updated to remove method chaining.

The requestOneTimePayment and requestBillingAgreement methods on PayPalClient have been updated to expect instances of PayPalCheckoutRequest and PayPalVaultRequest, respectively.

However, requestOneTimePayment and requestBillingAgreement have been deprecated in favor of tokenizePayPalAccount.

Samsung Pay

The SamsungPay feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:samsung-pay:4.47.0'
}

To use the feature, instantiate a SamsungPayClient:

package com.my.app;

public class SamsungPayActivity extends AppCompatActivity implements SamsungPayListener {
    
  private BraintreeClient braintreeClient;
  private SamsungPayClient samsungPayClient;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    samsungPayClient = new SamsungPayClient(braintreeClient);
  }

  private void checkIfSamsungPayIsAvailable() {
    samsungPayClient.isReadyToPay((isReadyToPay, error) -> {
      if (isReadyToPay) {
        // Samsung Pay is available 
      } else {
        // handle error 
      }
    });
  }
 
  private void launchSamsungPay() {
    samsungPayClient.buildCustomSheetPaymentInfo((builder, error) -> {
      if (builder != null) {
        CustomSheetPaymentInfo paymentInfo = builder
            .setAddressInPaymentSheet(CustomSheetPaymentInfo.AddressInPaymentSheet.NEED_BILLING_AND_SHIPPING)
            .setCustomSheet(getCustomSheet())
            .setOrderNumber("order-number")
            .build();
        samsungPayClient.startSamsungPay(paymentInfo, SamsungPayActivity.this);
      } else {
        // handle error
      }
    });
  }

  private CustomSheet getCustomSheet() {
    CustomSheet sheet = new CustomSheet();

    final AddressControl billingAddressControl = new AddressControl("billingAddressId", SheetItemType.BILLING_ADDRESS);
    billingAddressControl.setAddressTitle("Billing Address");
    billingAddressControl.setSheetUpdatedListener((controlId, customSheet) -> {
      samsungPayClient.updateCustomSheet(customSheet);
    });
    sheet.addControl(billingAddressControl);

    final AddressControl shippingAddressControl = new AddressControl("shippingAddressId", SheetItemType.SHIPPING_ADDRESS);
    shippingAddressControl.setAddressTitle("Shipping Address");
    shippingAddressControl.setSheetUpdatedListener((controlId, customSheet) -> {
      samsungPayClient.updateCustomSheet(customSheet);
    });
    sheet.addControl(shippingAddressControl);

    AmountBoxControl amountBoxControl = new AmountBoxControl("amountID", "USD");
    amountBoxControl.setAmountTotal(1.0, AmountConstants.FORMAT_TOTAL_PRICE_ONLY);
    sheet.addControl(amountBoxControl);

    return sheet;
  }

  @Override
  public void onSamsungPayStartError(@NonNull Exception error) {
    // handle samsung pay start error
  }

  @Override
  public void onSamsungPayStartSuccess(@NonNull SamsungPayNonce samsungPayNonce, @NonNull CustomSheetPaymentInfo paymentInfo) {
    // send samsungPayNonce.getString() to server to create a transaction
  }

  @Override
  public void onSamsungPayCardInfoUpdated(@NonNull CardInfo cardInfo, @NonNull CustomSheet customSheet) {
    // make adjustments to custom sheet as needed
    samsungPayClient.updateCustomSheet(customSheet);
  }
}

Visa Checkout

Visa Checkout is not yet supported in v4.

Union Pay

The Union Pay feature is now supported by implementing the following dependencies:

dependencies {
  implementation 'com.braintreepayments.api:union-pay:4.47.0'
}

To use the feature, instantiate a UnionPayClient:

package com.my.app;

public class UnionPayActivity extends AppCompatActivity {

  private BraintreeClient braintreeClient;
  private UnionPayClient unionPayClient;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    unionPayClient = new UnionPayClient(braintreeClient);
  }
  
  private void fetchUnionPayCapabilities() {
    unionPayClient.fetchCapabilities("4111111111111111", (capabilities, error) -> {
      if (capabilities != null) {
        // inspect Union Pay capabilities
      } else {
        // handle error
      }
    });
  }

  private void enrollUnionPay() {
    UnionPayCard unionPayCard = new UnionPayCard();
    unionPayCard.setNumber("4111111111111111");
    unionPayCard.setExpirationMonth("12");
    unionPayCard.setExpirationYear("22");
    unionPayCard.setCvv("123");
    unionPayCard.setPostalCode("12345");
    unionPayCard.setMobileCountryCode("1");
    unionPayCard.setMobilePhoneNumber("1234567890");

    unionPayClient.enroll(unionPayCard, (enrollment, error) -> {
      unionPayCard.setSmsCode("1234");
      unionPayCard.setEnrollmentId(enrollment.getId());
      tokenizeUnionPay(unionPayCard);
    });
  }

  private tokenizeUnionPay(UnionPayCard unionPayCard) {
    unionPayClient.tokenize(unionPayCard, (cardNonce, error) -> {
      if (cardNonce != null) {
        // send this nonce to your server
        String nonce = cardNonce.getString();
      } else {
        // handle error
      }
    });
  }
}

Venmo

The Venmo feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:venmo:4.47.0'
}

To use the feature, instantiate a VenmoClient:

package com.my.app;

public class VenmoActivity extends AppCompatActivity implements VenmoListener {

  private BraintreeClient braintreeClient;
  private VenmoClient venmoClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    venmoClient = new VenmoClient(this, braintreeClient);
    venmoClient.setListener(this);
  }

  // The authorizeAccount() method has been replaced with tokenizeVenmoAccount()
  private void tokenizeVenmoAccount() {
    VenmoRequest request = new VenmoRequest(VenmoPaymentMethodUsage.MULTI_USE);
    request.setProfileId("your-profile-id");
    request.setShouldVault(false);
          
    venmoClient.tokenizeVenmoAccount(this, request);
  }

  @Override
  public void onVenmoSuccess(@NonNull VenmoAccountNonce venmoAccountNonce) {
      // send nonce to server
  }

  @Override
  public void onVenmoFailure(@NonNull Exception error) {
      // handle error
  }
}

3D Secure

The 3D Secure feature is now supported in a single dependency:

dependencies {
  implementation 'com.braintreepayments.api:three-d-secure:4.47.0'
}

Additionally, add the following Maven repository and (non-sensitive) credentials to your app-level gradle:

repositories {
    maven {
        url "https://cardinalcommerceprod.jfrog.io/artifactory/android"
        credentials {
            username 'braintree_team_sdk'
            password 'cmVmdGtuOjAxOjIwMzgzMzI5Nzg6Q3U0eUx5Zzl5TDFnZXpQMXpESndSN2tBWHhJ'
        }
    }
}

To use the feature, instantiate a ThreeDSecureClient:

package com.my.app;

public class ThreeDSecureActivity extends AppCompatActivity implements ThreeDSecureListener {

  private BraintreeClient braintreeClient;
  private ThreeDSecureClient threeDSecureClient;
  private CardClient cardClient;
    
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    braintreeClient = new BraintreeClient(this, MerchantClientTokenProvider());
    threeDSecureClient = new ThreeDSecureClient(braintreeClient);
    threeDSecureClient.setListner(this);

    // you will also need a card client for tokenization in this example
    cardClient = new CardClient(braintreeClient);
  }

  @Override
  protected void onNewIntent(Intent newIntent) {
    super.onNewIntent(newIntent);
    // required if your activity's launch mode is "singleTop", "singleTask", or "singleInstance"
    setIntent(newIntent);
  }

  private void tokenizeCard() {
    Card card = new Card();
    card.setNumber("378282246310005");
    card.setExpirationDate("12/2022"); 

    cardClient.tokenize(card, (cardNonce, error) -> {
      if (cardNonce != null) {
        performThreeDSecureVerification(cardNonce);
      } else {
        // handle error
      }
    });
  }
 
  private void performThreeDSecureVerification(CardNonce cardNonce) {
    ThreeDSecurePostalAddress billingAddress = new ThreeDSecurePostalAddress();
    billingAddress.setGivenName("Jill");
    billingAddress.setSurname("Doe");
    billingAddress.setPhoneNumber("5551234567");
    billingAddress.setStreetAddress("555 Smith St");
    billingAddress.setExtendedAddress("#2");
    billingAddress.setLocality("Chicago");
    billingAddress.setRegion("IL");
    billingAddress.setPostalCode("12345");
    billingAddress.setCountryCodeAlpha2("US");

    ThreeDSecureAdditionalInformation additionalInformation = new ThreeDSecureAdditionalInformation();
    additionalInformation.accountId("account-id");

    ThreeDSecureRequest threeDSecureRequest = new ThreeDSecureRequest();
    threeDSecureRequest.setAmount("10");
    threeDSecureRequest.setEmail("[email protected]");
    threeDSecureRequest.setBillingAddress(billingAddress);
    threeDSecureRequest.setNonce(cardNonce.getString());
    threeDSecureRequest.setShippingMethod(ThreeDSecureShippingMethod.GROUND);
    threeDSecureRequest.setAdditionalInformation(additionalInformation);

    threeDSecureClient.performVerification(this, threeDSecureRequest, (threeDSecureResult, error) -> {
      if (threeDSecureResult != null) {
        // examine lookup response (if necessary), then continue verification
        threeDSecureClient.continuePerformVerification(ThreeDSecureActivity.this, threeDSecureRequest, threeDSecureResult);
      } else {
        // handle error
      }
    });
  }
  
  @Override
  public void onThreeDSecureSuccess(@NonNull ThreeDSecureResult threeDSecureResult) {
      // send this nonce to your server
      String nonce = threeDSecureResult.getTokenizedCard().getString();  
  }

  @Override
  public void onThreeDSecureFailure(@NonNull Exception error) {
      // handle error
  }
}

3DS1 UI Customization

The ThreeDSecureV1UiCustomization class setters have been updated to remove method chaining and follow standard Java getter/setter pattern.

3DS2 UI Customization

On ThreeDSecureRequest the uiCustomization property was replaced with v2UiCustomization of type ThreeDSecureV2UiCustomization. For 3DS2 UI customization, use the following new classes:

  • ThreeDSecureV2UiCustomization
  • ThreeDSecureV2ButtonCustomization
  • ThreeDSecureV2LabelCustomization
  • ThreeDSecureV2TextBoxCustomization
  • ThreeDSecureV2ToolbarCustomization

Default 3DS Version

Previously, the versionRequested property on ThreeDSecureRequest defaulted to VERSION_1. It now defaults to VERSION_2.

Shipping Method

The shippingMethod property on ThreeDSecureRequest is now an enum rather than a string. Possible values:

  • SAME_DAY
  • EXPEDITED
  • PRIORITY
  • GROUND
  • ELECTRONIC_DELIVERY
  • SHIP_TO_STORE

Integrating Multiple Payment Methods

Several features of the SDK require handling a browser switch result or an activity result. These can be handled in the same or different Activity/Fragment. If handled together, the results of each flow will be returned to the respective listener:

package com.my.app;

public class PaymentsActivity extends AppCompatActivity implements PayPalListener, VenmoListener {
  
  ...

  @Override
  protected void onPayPalSuccess(PayPalAccountNonce payPalAccountNonce) {
      // send nonce to your server
  }

  @Override
  protected void onPayPalFailure(Exception error) {
      // handle PayPal error 
  }

  @Override
  protected void onVenmoSuccess(VenmoAccountNonce venmoAccountNonce) {
      // send nonce to your server
  }

  @Override
  protected void onVenmoFailure(Exception error) {
      // handle Venmo error 
  }
}

Manual Browser Switching for Browser-Based Flows

For more control over browser-based payment flows, consider the manual browser switch integration pattern.

Here is an example of manual browser switching using PayPalClient:

class MainActivity : AppCompatActivity() {

  private lateinit var payPalClient: PayPalClient

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val authorization = "<TOKENIZATION_KEY_OR_CLIENT_TOKEN>"
    val braintreeClient = BraintreeClient(this, authorization, "my-custom-url-scheme")
    payPalClient = PayPalClient(braintreeClient)

    val browserSwitchResult = payPalClient.parseBrowserSwitchResult(this, intent)
    if (browserSwitchResult != null) {
      // process kill scenario
      handleBrowserSwitchResult(browserSwitchResult)
    } else {
      // initiate tokenization
      val payPalRequest = PayPalCheckoutRequest("1.00")
      payPalClient.tokenizePayPalAccount(payPalRequest)
    }
  }

  override fun onResume() {
    super.onResume()
    payPalClient.parseBrowserSwitchResult(this, intent)?.let {
      handleBrowserSwitchResult(it)
    }
  }

  override fun onNewIntent(newIntent: Intent?) {
    super.onNewIntent(newIntent)
      
    // required if your activity's launch mode is "singleTop", "singleTask", or "singleInstance"
    payPalClient.parseBrowserSwitchResult(this, newIntent)?.let {
      handleBrowserSwitchResult(it)
    }
  }

  private fun handleBrowserSwitchResult(result: BrowserSwitchResult) {
    payPalClient.onBrowserSwitchResult(result) { payPalAccountNonce, error ->
      payPalAccountNonce?.let {
        // forward nonce to server
      } ?: error?.let {
        // handle error
      }
    }

    // clear pending request to guard against additional browser switch result invocations
    payPalClient.clearActiveBrowserSwitchRequests(this)
  }
}