Using Android Standard UI Components
Learn how to build an integration that uses the built-in user interface components in Stripe's Android SDK.
This guide demonstrates how to build your app's customer payment management flow using PaymentSession, a class designed to make building your app's checkout flow as easy as possible. It handles collecting, saving, and reusing your user's payment details.
Prepare your API
Our prebuilt UI elements operate on the assumption that you have a single Customer
object for each of your users. We strongly recommend you create this Customer
at the same time you create your user on your own backend, so that every user is guaranteed to have an associated Customer
. (This is fine even if you don't collect your user's payment information when they sign up—it's totally OK to have a Customer without any attached cards.)
Creating ephemeral keys
Our prebuilt UI elements require an ephemeral key, a short-lived API key with restricted API access. You can think of an ephemeral key as a session, authorizing the SDK to retrieve and update a specific Customer object for the duration of the session. To provide an ephemeral key to the SDK, you'll need to expose a new API endpoint on your backend. This endpoint should create an ephemeral key for the current Stripe customer, and return the key's unmodified response as JSON.
When the SDK requests an ephemeral key, it specifies the version of the Stripe API that it expects the response to come from. Your endpoint should accept an api_version
parameter, and use the specified API version when creating the ephemeral key. This ensures that the SDK always receives the correct ephemeral key response from your backend. You can consult our Example Backend to see this in practice.
# Sinatra
post path do
stripe_version = params['api_version']
customer_id = session['customer_id']
key = Stripe::EphemeralKey.create(
{customer: customer_id},
{stripe_version: stripe_version}
)
key.to_json
end
# Flask
from flask import Flask, session, jsonify, request
# This function assumes that the session handling has stored the customerId
@app.route(path, methods=['POST'])
def issue_key():
stripe_api_version = request.args['api_version']
customerId = session['customerId']
key = stripe.EphemeralKey.create(customer=customerId, api_version=stripe_api_version)
return jsonify(key)
// This assumes that $customerId has been set appropriately from session data
parse_str($_SERVER['QUERY_STRING'], $params);
if (!isset($params['api_version']))
{
exit(http_response_code(400));
}
try {
$key = \Stripe\EphemeralKey::create(
["customer" => $customerId],
["stripe_version" => $params['api_version']]
);
header('Content-Type: application/json');
exit(json_encode($key));
} catch (Exception $e) {
exit(http_response_code(500));
}
// Express
app.post(path, (req, res) => {
const stripe_version = req.query.api_version;
if (!stripe_version) {
res.status(400).end();
return;
}
// This function assumes that some previous middleware has determined the
// correct customerId for the session and saved it on the request object.
stripe.ephemeralKeys.create(
{customer: req.customerId},
{stripe_version: stripe_version}
).then((key) => {
res.status(200).json(key);
}).catch((err) => {
res.status(500).end();
});
});
// net/http
// The customerId parameter should be the ID of the Customer object associated
// with the session the request was made on.
func issueKeyHandler(w http.ResponseWriter, r *http.Request, customerId string) {
r.ParseForm()
stripeVersion := r.Form.Get("api_version")
if stripeVersion == "" {
log.Printf("Stripe-Version not found\n")
w.WriteHeader(400)
return
}
params := &stripe.EphemeralKeyParams{
Customer: stripe.String(customerId),
StripeVersion: stripe.String(stripeVersion),
}
key, err := ephemeralkey.New(params)
if err != nil {
log.Printf("Stripe bindings call failed, %v\n", err)
w.WriteHeader(500)
return
}
w.Write(key.RawJSON)
}
After you've added an ephemeral key endpoint to your backend, you'll need a way for your Android app to communicate with this endpoint. In your app, you should make your API client class implement the EphemeralKeyProvider
interface, which defines a single method, createEphemeralKey
. When implementing this method, be sure to pass the apiVersion
parameter along to your ephemeral keys endpoint. You can consult our Example App to see this in practice.
// Using RxJava2 for handling asynchronous responses
@Override
public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion,
@NonNull final EphemeralKeyUpdateListener keyUpdateListener) {
final Map<String, String> apiParamMap = new HashMap<>();
apiParamMap.put("api_version", apiVersion);
mCompositeDisposable.add(mStripeService.createEphemeralKey(apiParamMap)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
try {
final String rawKey = response.string();
keyUpdateListener.onKeyUpdate(rawKey);
mProgressListener.onStringResponse(rawKey);
} catch (IOException ignored) {
}
},
throwable ->
mProgressListener.onStringResponse(throwable.getMessage())));
}
After creating an EphemeralKeyProvider, initialize a CustomerSession
. Think of CustomerSession
as a Stripe communication manager for a single logged-in session of a customer. A CustomerSession
talks to your backend to retrieve an ephemeral key (via its key provider), and uses that key to manage retrieving and updating its Stripe customer on your behalf.
CustomerSession.initCustomerSession(this,
new ExampleEphemeralKeyProvider(
new ExampleEphemeralKeyProvider.ProgressListener() {
@Override
public void onStringResponse(String string) {
if (string.startsWith("Error: ")) {
// Show the error to the user.
}
}
}));
CustomerSession
automatically prefetches its customer once its key provider has been set. If you'd like to take advantage of preloading your customer's information, initialize your CustomerSession
instance earlier, before your user enters your payment flow. If your current user logs out of the app, you should clear the current CustomerSession
singleton using the clearInstance
method. When a new user logs in, you can re-initialize the instance. On your backend, be sure to create and return a new ephemeral key for the customer object associated with the new user.
Implement your app's payment flow using PaymentSession
Our SDK provides a class called PaymentSession
, which is designed to make building your app's checkout flow as easy as possible. It handles collecting, saving, and reusing your user's payment details, and can also be used to collect shipping info. Think of it as the data source for your checkout activity—it handles asynchronously retrieving the data you need, and notifies its listener when its UI should change.
Setting the listener, configuration, and host activity
To work with PaymentSession
, you'll need to write a class that conforms to PaymentSessionListener
, and pass a PaymentSessionConfig
object to the init
method. (Note, the code samples in this section are simply examples – your own implementation may differ depending on the structure of your app).
The PaymentSessionConfig
object is created using a Builder pattern, and has the following fields, all of which are optional.
setShippingInfoRequired(boolean required)
This method sets whether or not the PaymentSession requires the user
to have a valid shipping address. The default value is true
.
setShippingMethodsRequired(boolean required)
This method sets whether or not the PaymentSession requires the user
to select a shipping method (Overnight, Standard Shipping, etc). The default value is true
. You must set these values later.
setPrepopulatedShippingInfo(ShippingInformation shippingInfo)
Sets a default shipping address for your customer, which you can get from your CustomerSession or your own records. The Shipping information screen will start with this information already filled out.
setHiddenShippingInfoFields(List hiddenFields)
Sets which address fields should be hidden in the shipping information screen. Values must be from among the labels listed in ShippingInfoWidget.
. All fields will be shown if this list is empty. Note that not all fields can be hidden, such as country or name.
setOptionalShippingInfoFields(List hiddenFields)
Sets which address fields should be listed as optional in the shipping information screen. Values must be from among the labels listed in ShippingInfoWidget.
. All fields will be required if this list is empty. Note that not all fields can be optional, such as country or name.
Having created the PaymentSessionConfig
, you'll need to create your PaymentSessionListener
, which has 3 required methods:
void onPaymentSessionDataChanged(@NonNull PaymentSessionData data)
This method is called whenever the PaymentSession's data changes, e.g., when the user selects a new PaymentMethod
or enters shipping info. This is a good place to update your UI:
@Override
public void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) {
final PaymentMethod selectedPaymentMethod = data.getPaymentMethod();
final PaymentMethod.Card card = selectedPaymentMethod.card;
// Display information about the selected card
// Update your UI here with other data
if (data.isPaymentReadyToCharge()) {
// Use the data to complete your charge - see below.
}
}
This method should also check for whether or not the payment data is complete, according to the PaymentSessionConfig
specified. If you receive an update for which PaymentSessionData#isPaymentReadyToCharge()
returns true, you can immediately send a message to your server to complete the charge.
void onCommunicatingStateChanged(boolean isCommunicating)
This method is called whenever the network communication state has changed. We recommend showing a spinner or infinite progress bar when it is set to true
void onCommunicatingStateChanged(boolean isCommunicating) {
if (isCommunicating) {
mHostActivity.getProgressBar().setVisibility(View.VISIBLE);
} else {
mHostActivity.getProgressBar().setVisibility(View.GONE);
}
}
void onError(int errorCode, @Nullable String errorMessage)
This method is called whenever an error occurs when connecting to the Stripe API. The error messages should be user-surfaceable, so displaying them in an alert dialog is recommended.
Using the PaymentSession
to collect data
Having created your PaymentSessionConfig
and PaymentSessionListener
, you can now initialize the PaymentSession
. In the below example, we use anonymous classes to create our listener and config for simplicity.
private void setupPaymentSession() {
mPaymentSession = new PaymentSession(this);
final boolean paymentSessionInitialized = mPaymentSession.init(
new PaymentSession.PaymentSessionListener() {
@Override
public void onCommunicatingStateChanged(boolean isCommunicating) {
if (isCommunicating) {
mProgressBar.setVisibility(View.VISIBLE);
} else {
mProgressBar.setVisibility(View.INVISIBLE);
}
}
@Override
public void onError(int errorCode, @Nullable String errorMessage) {
mErrorDialogHandler.showError(errorMessage);
}
@Override
public void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) {
mResultTitleTextView.setVisibility(View.VISIBLE);
mResultTextView.setText(formatStringResults(mPaymentSession.getPaymentSessionData()));
}
},
new PaymentSessionConfig.Builder()
.setPrepopulatedShippingInfo(getExampleShippingInfo())
.build()
);
if (paymentSessionInitialized) {
mStartPaymentFlowButton.setEnabled(true);
}
}
Note that the init
function returns a boolean
value to let you know whether or not the initialization was successful. The only reason for failure is if you have not already set up a CustomerSession
, which must come first.
Once the PaymentSession
has been initialized, you can use it to make the following calls.
void presentPaymentMethodSelection()
This method starts the PaymentMethodsActivity
to allow the user to choose a stored payment method or to add new payment methods and select from among those. The payment method list will automatically be populated with existing payment methods belonging to the Customer.
void presentShippingFlow()
This method presents the PaymentFlowActivity to allow the user to enter shipping information, if such information is required according to your PaymentSessionConfig
. Note that in order to validate the input shipping address, you'll need to register a local BroadcastReceiver
.
@Override
protected void onCreate(Bundle savedInstance) {
// Create the BroadcastReceiver in onCreate
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
ShippingInformation shippingInformation = intent.getParcelableExtra(EXTRA_SHIPPING_INFO_DATA);
Intent shippingInfoProcessedIntent = new Intent(EVENT_SHIPPING_INFO_PROCESSED);
if (shippingInformation.getAddress() == null || !shippingInformation.getAddress().getCountry().equals(Locale.US.getCountry())) {
// In this example, we check to see that the country is US. Your logic will probably be more complicated.
shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, false);
} else {
ArrayList<ShippingMethod> shippingMethods = createSampleShippingMethods();
shippingInfoProcessedIntent.putExtra(EXTRA_IS_SHIPPING_INFO_VALID, true);
shippingInfoProcessedIntent.putParcelableArrayListExtra(EXTRA_VALID_SHIPPING_METHODS, shippingMethods);
shippingInfoProcessedIntent.putExtra(EXTRA_DEFAULT_SHIPPING_METHOD, shippingMethods.get(1));
}
LocalBroadcastManager.getInstance(PaymentSessionActivity.this).sendBroadcast(shippingInfoProcessedIntent);
}
};
// Register in onCreate
LocalBroadcastManager.getInstance(this).registerReceiver(mBroadcastReceiver,new IntentFilter(EVENT_SHIPPING_INFO_SUBMITTED));
}
@Override
public void onDestroy() {
// Don't forget to unregister in onDestroy!
LocalBroadcastManager.getInstance(this).unregisterReceiver(mBroadcastReceiver);
}
When you broadcast your response to the shipping validation request, you can then send down your (potentially) dynamically created shipping methods. This allows you to validate whether or not a customer is eligible for expedited shipping, for instance, based on the input shipping address. This is also the place to filter out locations that you cannot ship to, for instance if you are restricted to domestic customers.
Completing the purchase
Once PaymentSession#isPaymentReadyToCharge()
returns true
, charge the PaymentMethod
with the data in the PaymentSessionData
object using a PaymentIntent. After the payment is captured, call PaymentSession#onCompleted()
.
Managing PaymentSession in a host Activity
In order to get updates for the PaymentSessionData
object and to handle state during Activity lifecycle, you’ll need to hook up your PaymentSession
instance to a few key parts of your host Activity lifecycle. The first is in onActivityResult()
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mPaymentSession.handlePaymentData(requestCode, resultCode, data);
}
This is all you need to do to get updates from the various activities launched by PaymentSession
. Any updates to the data will be reported to the PaymentSessionListener argument to PaymentSession#init().
To handle lifecycle events in your activity that require the saving and restoring of state, the session can be attached and re-initialized as follows. If saving state during activity lifecycle events is important, be sure to pass the savedInstanceState
to PaymentSession#init()
.
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mPaymentSession.savePaymentSessionInstanceState(outState);
}
// Can also be re-initialized in onRestoreInstanceState
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// other onCreate logic
mPaymentSession = new PaymentSession(this);
// Create your listener and your configuration
// ...
// It's fine if the savedInstanceState is null.
mPaymentSession.init(mPaymentSessionListener, mPaymentSessionConfig, savedInstanceState);
}
Finally, don’t forget to destroy the PaymentSession
when your activity is destroyed. This clears out any listeners and references that you may have given the PaymentSession object that could have been put in different threads.
@Override
protected void onDestroy() {
mPaymentSession.onDestroy();
super.onDestroy();
}