Managing Customer Information in the Android SDK

    Allow customers to securely store and update payment information.

    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.)

    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(
          array("customer" => $customerId),
          array("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
        }
        key, err := ephemeralkey.New(&stripe.EphemeralKeyParams{
            Customer:      customerId,
            StripeVersion: stripeVersion,
        })
        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 RxJava for handling asynchronous responses
    @Override
    public void createEphemeralKey(@NonNull @Size(min = 4) String apiVersion,
                                 @NonNull final EphemeralKeyUpdateListener keyUpdateListener) {
      Map<String, String> apiParamMap = new HashMap<>();
      apiParamMap.put("api_version", apiVersion);
    
      mCompositeSubscription.add(
              mStripeService.createEphemeralKey(apiParamMap)
                      .subscribeOn(Schedulers.io())
                      .observeOn(AndroidSchedulers.mainThread())
                      .subscribe(new Action1<ResponseBody>() {
                          @Override
                          public void call(ResponseBody response) {
                              try {
                                  String rawKey = response.string();
                                  keyUpdateListener.onKeyUpdate(rawKey);
                                  mProgressListener.onStringResponse(rawKey);
                              } catch (IOException iox) {
    
                              }
                          }
                      }, new Action1<Throwable>() {
                          @Override
                          public void call(Throwable 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(
            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 payment method or enters shipping info. This is a good place to update your UI:

    @Override
    public void onPaymentSessionDataChanged(@NonNull PaymentSessionData data) {
      final String selectedPaymentMethodId = data.getSelectedPaymentMethodId();
      CustomerSession.getInstance().retrieveCurrentCustomer(
        new CustomerRetrievalListener() {
          @Override
          public void onCustomerRetrieved(@NonNull Customer customer) {
            Source displaySource = customer.getSourceById(selectedPaymentMethodId);
            // Display the card information on your screen.
          }
    
          @Override
          public void onError(int errorCode, @Nullable String errorMessage){
            Toast.makeText(HostActivity.this, errorMesage, Toast.LENGTH_SHORT).show();
          }
        }
      );
    
      // Update your UI here with other data
      if (data.isPaymentReadyToCharge() && data.getPaymentResult == PaymentResultListener.INCOMPLETE) {
        // 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 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 . In the below example, we use anonymous classes to create our listener and config for simplicity.

        private void setupPaymentSession() {
        mPaymentSession = new PaymentSession(this);
        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 presents the PaymentMethodsActivity to allow the user to choose a stored form of payment or to add more forms of payment and select from among those. It will automatically be pre-populated with values from your CustomerSession, so previously existing payment sources will appear without any extra work.

    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

    When you have detected that the PaymentSession#isPaymentReadyToCharge() has come back true, you are free to charge the payment method immediately with the data in the PaymentSessionData object using a call to your server. To make full use of the PaymentSession and guard against duplicate payment requests, we recommend making use of the completePayment method.

    void completePayment(@NonNull PaymentCompletionProvider provider)

    This method will call the complete payment method on the provider, talking to your server to make the purchase, and recording the result data in the PaymentSessionData To use this method, you must provide a PaymentCompletionProvider, which requires the implementation of just one method:

    void completePayment(@NonNull PaymentSessionData data, @NonNull PaymentResultListener listener)

    This method will be invoked with the current PaymentSessionData object, and the results will be returned to the PaymentResultListener (which you do not need to create). Any errors will be returned to your original PaymentSessionListener, and a successful update will result in a call to the same onPaymentSessionDataChanged method used to update the listener about any other change in the PaymentSessionData. To verify results, call PaymentSessionData#getPaymentResult(), which returns a PaymentResult.

    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 listener attached during the initialization of the PaymentSession. 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:

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        mPaymentSession.handlePaymentData(requestCode, resultCode, data);
    }

    If saving state during activity lifecycle events is important, be sure to pass the savedInstanceState into the init method of the PaymentSession.

    @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() {
        super.onDestroy();
        mPaymentSession.onDestroy();
    }