Tutorial

This tutorial walks you through the steps involved in integrating Android Pay into a purchase flow. The guide provides conceptual details and example source code to help you complete the integration steps summarized in the API overview, with the end goal of reproducing a flow similar to the Android Pay API Process Flow.

For the purposes of this guide, we assume that you are familiar with the basic concepts and skills of application development for the Android platform. If you need to learn about Android development before getting started, work through some lessons in the Training for Android Developers. To dive into the details of the Android APIs, see the API Reference.

Note: With the integration described here, merchants can process payments using their existing payment infrastructure. If you are looking for a solution where Google processes payments for you, use the Android In-App Billing SDK.

Contents

Preparation

Get the sample app

You can download a sample app that demonstrates the implementation described here via GitHub.

Configure your project

If you have not viewed the Setup section yet, please review it now to configure your project.

Review your Payment Processor’s Documentation

If you see your payment processor listed here, please familiarize yourself with their documentation specific to Android Pay.

  Adyen Bank of America Merchant Services Braintree Cardstream Creditcall Cybersource Elavon First Data Global Payments Judo Klarna Simplify Stripe Vantiv Worldnet Worldpay Zooz

Code and Test

About constants

Within the sample app, and included in sections of this guide, we reference a Constants class. This class is not part of the Google APIs, but is suggested as a convenient way to define, update, and use constant values in your app. Here is a short example of creating a Constants class:

public class Constants {
     public static final int WALLET_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST;   
     public static final String MERCHANT_NAME = "Awesome Bike Store";    
     public static final String CURRENCY_CODE_USD = "USD";
}

For the complete set of constants defined in the sample app, see the sample Constants.java file.

The value of the Environment parameter in WalletOptions indicates whether the server is running in a production or test environment. Its value can be WalletConstants.ENVIRONMENT_PRODUCTION or WalletConstants.ENVIRONMENT_TEST. For testing and development, always use the test environment only.

Is the user ready to pay with Android Pay?

Before starting the Android Pay flow, use the isReadyToPay() method to check whether the user has the Android Pay app installed and is ready to pay. If this method returns true, show the Android Pay button. If it returns false, display other checkout options along with text notifying the user to set up the Android Pay app.

// Show spinner or other UI cue to indicate progress  
showProgressDialog();  
Wallet.Payments.isReadyToPay(mGoogleApiClient).setResultCallback(  
    new ResultCallback<BooleanResult>() {  
    @Override  
    public void onResult(@NonNull BooleanResult booleanResult) {  
        hideProgressDialog();  
        if (booleanResult.getStatus().isSuccess()) {  
            if (booleanResult.getValue()) {  
                // Show Android Pay buttons alongside regular checkout button  
                // ...  
            } else {  
                // Hide Android Pay buttons, show a message that Android Pay  
                // cannot be used yet, and display a traditional checkout button  
                // ...  
            }  
        } else {  
            // Error making isReadyToPay call  
            Log.e(TAG, "isReadyToPay:" + booleanResult.getStatus());  
        }  
    }  
});

Request a Payment Token

Before you can request any wallet information, you must build parameters to inform the Android Pay API what type of payment token you wish to receive in the response. The PaymentMethodTokenizationParameters builder lets you establish the parameters of the token. The inputs you provide to this call depend on how you plan to process payments, and may also vary depending on your payment processor provider. You will initialize either a network token or a payment gateway token.

Request a payment gateway token

If you use Braintree, Stripe, or Vantiv to process payments, you can set provider-specific parameters (a publishable key) to receive a gateway token that you can charge with your provider. Please refer to the Braintree, Stripe or Vantiv developer documentation for more information.

The following example shows how to request a token from Stripe:

PaymentMethodTokenizationParameters parameters = 
    PaymentMethodTokenizationParameters.newBuilder()          
        .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.PAYMENT_GATEWAY)  
            .addParameter("gateway", "stripe")  
            .addParameter("stripe:publishableKey", publishableKey)  
            .addParameter("stripe:version", version)  
            .build();

Request a network token

Set the tokenization type and add a publicKey parameter as shown:

PaymentMethodTokenizationParameters parameters =  
    PaymentMethodTokenizationParameters.newBuilder()              
      .setPaymentMethodTokenizationType(PaymentMethodTokenizationType.NETWORK_TOKEN)  
        .addParameter("publicKey", publicKey)  
        .build();

The publicKey parameter is the base-64 encoded version of your encryption key. For more information about generating this key, see Using OpenSSL to Generate and Format a Key.
We recommend working with your payment provider to set your public key and manage decryption of Android Pay credentials. Alternatively, if you are familiar with elliptic curve cryptography, see Setting a public key for guidance.

Create a Masked Wallet request

You must create an instance of MaskedWalletRequest to retrieve the Masked Wallet information (such as billing address, shipping address, masked backing payment instrument number, and cart items) from the Android Pay API. The MaskedWalletRequest object must be passed in when you initialize the purchase button in the next section.

At this point, you won't have the user's chosen shipping address from Android Pay, so you must create an estimate of the shipping costs and tax. If you set the shopping cart as shown below, make sure the cart total matches the sum of the line items added to the cart.

The following example creates the Masked Wallet request using the builder pattern:

MaskedWalletRequest request = MaskedWalletRequest.newBuilder()  
    .setMerchantName(Constants.MERCHANT_NAME)  
    .setPhoneNumberRequired(true)  
    .setShippingAddressRequired(true)  
    .setCurrencyCode(Constants.CURRENCY_CODE_USD)  
    .setEstimatedTotalPrice(cartTotal)  
    // Create a Cart with the current line items. Provide all the information  
    // available up to this point with estimates for shipping and tax included.  
    .setCart(Cart.newBuilder()  
        .setCurrencyCode(Constants.CURRENCY_CODE_USD)  
        .setTotalPrice(cartTotal)  
        .setLineItems(lineItems)  
        .build())  
    .setPaymentMethodTokenizationParameters(parameters)  
    .build();

We recommend using addAllowedCardNetworks to ensure you only receive credit cards your payments backend can successfully process.

Note: In this step, you're requesting masked payment and shipping information to update estimate shipping fees and taxes, and to display order details to the user before confirmation.

Add a purchase button with WalletFragment

About WalletFragment

The Android Pay API provides the WalletFragment class to handle user events and to automate key parts of the purchase lifecycle. As illustrated in this guide and the sample, an app typically uses instances of the wallet fragment class in two key areas of the purchase flow:

  1. A purchase fragment (defined by WalletFragmentMode.BUY_BUTTON), which displays a purchase button and sets a Masked Wallet request to send to Android Pay.

    buy button
  2. A confirmation fragment (defined by WalletFragmentMode.SELECTION_DETAILS), which displays "change" buttons to let users optionally modify the masked wallet information (such as payment instrument, shipping address, etc.), and sets the Masked Wallet.

    change button

    The “Change” buttons are particularly important for preauthorized users, who will not see the Android Pay chooser when loadMaskedWallet is called. If the user clicks a “Change” button, the fragment returns a new Masked Wallet in onActivityResult of the enclosing activity.

Note: Although the examples here invoke the wallet fragment programmatically, you also can add it to an activity layout via XML. For more information, see WalletFragment.

The final calls for loading the Full Wallet are not included in the automated functionality of the WalletFragment class. Instead, these calls are encapsulated in the FullWalletConfirmationButtonFragment class of the sample app. Relevant calls are described in detail in Retrieve the Full Wallet.

Create the WalletFragment

Construct an instance of WalletFragment to add to your checkout activity:

WalletFragmentStyle walletFragmentStyle = new WalletFragmentStyle()  
    .setBuyButtonText(WalletFragmentStyle.BuyButtonText.BUY_WITH)          
    .setBuyButtonAppearance(WalletFragmentStyle.BuyButtonAppearance.ANDROID_PAY_DARK)  
    .setBuyButtonWidth(WalletFragmentStyle.Dimension.MATCH_PARENT);

WalletFragmentOptions walletFragmentOptions = WalletFragmentOptions.newBuilder()  
    .setEnvironment(Constants.WALLET_ENVIRONMENT)  
    .setFragmentStyle(walletFragmentStyle)  
    .setTheme(WalletConstants.THEME_LIGHT)  
    .setMode(WalletFragmentMode.BUY_BUTTON)  
    .build();

mWalletFragment = SupportWalletFragment.newInstance(walletFragmentOptions);

The value of the Constants.WALLET_ENVIRONMENT parameter while you are in test must be WalletConstants.ENVIRONMENT_TEST. Note that the example code uses its own Constants class to maintain this setting.

When you initialize the purchase fragment, pass in the maskedWalletRequest that you created in the previous step, as well as the code REQUEST_CODE_MASKED_WALLET used to uniquely identify this call in the onActivityResult() callback:

WalletFragmentInitParams.Builder startParamsBuilder = 
    WalletFragmentInitParams.newBuilder()  
        .setMaskedWalletRequest(maskedWalletRequest)  
        .setMaskedWalletRequestCode(REQUEST_CODE_MASKED_WALLET)  
        .setAccountName(accountName);  
mWalletFragment.initialize(startParamsBuilder.build());

// add Wallet fragment to the UI  
getSupportFragmentManager().beginTransaction()  
    .replace(R.id.dynamic_wallet_button_fragment, mWalletFragment)  
    .commit();

chooser dialog

Handle Masked Wallet Request Intents

When the user taps the buy button (or if you called loadMaskedWallet directly) the API retrieves the Masked Wallet and returns it in the onActivityResult() of the enclosing activity as shown below. If the user has not authorized this app to use their payment information for future purchases, Android Pay presents a chooser screen and returns control to the app.

If the Activity resultCode is RESULT_OK, the MaskedWallet instance includes the Google Transaction ID, which is Google's unique identifier for this transaction. This value is required in the googleTransactionId field on all subsequent change Masked Wallet and Full Wallet requests pertaining to this transaction.

At this point, the app has the shipping address and billing address, so it can calculate the exact total purchase price and display it. After the app obtains the Masked Wallet, it should present a confirmation page showing the total cost of the items purchased in the transaction, unless the app has already calculated and displayed a total price to the user.

@Override  
public void onActivityResult(int requestCode, int resultCode, Intent data) {  
    // retrieve the error code, if available  
    int errorCode = -1;  
    if (data != null) {  
        errorCode = data.getIntExtra(WalletConstants.EXTRA_ERROR_CODE, -1);  
    }  
    switch (requestCode) {  
        case REQUEST_CODE_MASKED_WALLET:  
            switch (resultCode) {  
                case Activity.RESULT_OK:  
                    if (data != null) {  
                        MaskedWallet maskedWallet =  
                            data.getParcelableExtra(WalletConstants.EXTRA_MASKED_WALLET);

                        // call to get the Google transaction id  
                        googleTransactionId = getGoogleTransactionId()

                        launchConfirmationPage(maskedWallet);  
                    }  
                    break;  
                case Activity.RESULT_CANCELED:  
                    break;  
                default:  
                    handleError(errorCode);  
                    break;  
            }  
            break;

        case WalletConstants.RESULT_ERROR:  
            handleError(errorCode);  
            break;

        default:  
            super.onActivityResult(requestCode, resultCode, data);  
            break;  
    }  
}

Request the Full Wallet

When the user confirms the order, you are ready to request the Full Wallet. The Full Wallet Request should have the total charge that you are requesting including exact shipping, handling, and tax. You must include the GoogleTransactionId that you received in the Masked Wallet returned in onActivyResult().

Create a FullWalletRequest object that contains the various line items, tax and shipping where applicable, and a Cart object.

FullWalletRequest request = FullWalletRequest.newBuilder()  
    .setGoogleTransactionId(mMaskedWallet.getGoogleTransactionId())  
    .setCart(Cart.newBuilder()  
            .setCurrencyCode(Constants.CURRENCY_CODE_USD)  
            .setTotalPrice(cartTotal)  
            .setLineItems(lineItems)  
            .build())  
    .build();

Retrieve the Full Wallet

Once you have constructed a FullWalletRequest, use the
Wallet.Payments.loadFullWallet(...) method to get the FullWallet object. Before you can call this method, you must construct a GoogleApiClient in the onCreate() method of your Activity.

mGoogleApiClient = new GoogleApiClient.Builder(getActivity())  
    .enableAutoManage(fragmentActivity, this /* onConnectionFailedListener */)  
    .setAccountName(accountName) // optional  
    .addApi(Wallet.API, new Wallet.WalletOptions.Builder()  
            .setEnvironment(Constants.WALLET_ENVIRONMENT)  
            .setTheme(WalletConstants.THEME_LIGHT)  
            .build())  
    .build();

After you construct the full wallet request instance, call the loadFullWallet() method. When the full wallet request is completed, Google calls the onActivityResult() method, passing the intent result:

Wallet.Payments.loadFullWallet(mGoogleApiClient, fullWalletRequest,  
    REQUEST_CODE_RESOLVE_LOAD_FULL_WALLET);

After you retrieve the Full Wallet in the callback, you have enough information to proceed to payment processing for this transaction. You can access the details of the payment credentials from the Full Wallet, as shown below:

// Get payment method token  
PaymentMethodToken token = fullWallet.getPaymentMethodToken();

// Get the JSON of the token object as a String  
String tokenJSON = token.getToken();

complete transaction If you are retrieving a network token, the next step is to decrypt and process the payment token. We recommend working with your payment processor to manage the decryption. Alternatively, if you have expertise in cryptography, you can parse and decrypt the token on a secure PCI-compliant server, as described in Retrieving the encrypted payload.

If you are using Stripe, or Braintree, or Vantiv, parse the token JSON, extract the id field, and send it to the payment processor. Please see your processor documentation for further details.

Once you've processed the payment, continue through your typical purchase flow to your receipt page.

Handle errors

Fetching error codes

Since a typical activity or fragment can call multiple Google API methods, it's advisable to have a common code block in onActivityResult()that fetches error codes.

int errorCode = -1;  
if (data != null) {  
    errorCode = data.getIntExtra(  
        WalletConstants.EXTRA_ERROR_CODE, -1);  
}

After you fetch the codes, you can handle errors in the default block of a switch/case statement by calling the handleError(errorCode) method:

void handleError(int errorCode) {  
    switch (errorCode) {  
        case WalletConstants.ERROR_CODE_INVALID_PARAMETERS:  
        case WalletConstants.ERROR_CODE_AUTHENTICATION_FAILURE:  
        case WalletConstants.ERROR_CODE_BUYER_ACCOUNT_ERROR:  
        case WalletConstants.ERROR_CODE_MERCHANT_ACCOUNT_ERROR:  
        case WalletConstants.ERROR_CODE_SERVICE_UNAVAILABLE:  
        case WalletConstants.ERROR_CODE_UNSUPPORTED_API_VERSION:  
        case WalletConstants.ERROR_CODE_UNKNOWN:  
        default:  
            // unrecoverable error  
            mGoogleWalletDisabled = true;  
            displayGoogleWalletErrorToast(errorCode);  
            break;  
    }  
}

You can augment this implementation to handle your app's specific needs. For example, if you support multiple payment methods, you can choose to use another payment method for unrecoverable errors.

Handling errors during the purchase flow

You must handle two types of errors from Android Pay: recoverable and unrecoverable. For unrecoverable errors, the best strategy is to return users to a screen or UI element where they can select from available payment methods — including Android Pay — in case they want to retry. This strategy lets you fall back on your native purchase flow or some other valid alternative payment method.

For issues that prevent initiation of the payment flow, disable the "Android Pay" button. Again, you can handle this by making sure that alternative payment methods are available.

For more information about the error codes, their causes, and suggested ways to handle recoverable errors, see the WalletConstants reference.

Investigating errors in your app's log

When the Android Pay API encounters an unrecoverable error, in many cases it will log a more detailed description of the error to the client logs.

Use this shell script to help find and deduce the cause of the error.

adb logcat | grep WalletMerchantError

This detailed statement, paired with the error code above, should give a good diagnostic of the issue.

Test your app for different locales and resolving invalid price formats

The Android Pay API expects prices to either be formatted as a string: "0.00", or as a regular expresssion: "[0-9]+(.[0-9][0-9])?". Make sure the prices returned in your purchase flow conform to this format.

When a user switches the locale on their device, for example Locale.ES or Locale.FR, a price in USD would return in the format, "0,00" with a comma in place of a decimal. This invalid format will cause an error.

Testing with different locales is essential to protect against submitting an invalid price format to the Android Pay API. You can test different locales by changing the language/locale of your Android device within the device's Settings.

To resolve price format errors, either use the BigDecimal class or use the DecimalFormat in the US locale.

To use BigDecimal for creating and setting the price in MaskedWalletRequest or FullWalletRequest:

BigDecimal priceDecimal = new BigDecimal(2.22d);  
// We only want 2 decimals after the period.  
priceDecimal = priceDecimal.setScale(2, RoundingMode.HALF_EVEN);  
String price = priceDecimal.toString();

Here, we instantiate a BigDecimal object, set the scale to 2 since we want only 2 decimal places, and then call toString() to get the price in the desired format.

To use DecimalFormat in the US locale:

DecimalFormat df = new DecimalFormat("0.00",  
   new DecimalFormatSymbols(Locale.US));  
String price = df.format(2.22d);

Here, we create an instance of DecimalFormat that will always use the US Locale irrespective of the system default.

Make sure you use the above techniques only for formatting the price string (setting the values in the Builder instance). When displaying the string to the user, always rely on the system default Locale, which should be well understood by the user of the device.

Next Step: Start Accepting Payments

Send feedback about...

Android Pay API
Need help? Visit our support page.