Using iOS Standard UI Components

    Learn how to accept payments in iOS applications with built-in support for Apple Pay.

    The STPPaymentContext class helps you build the checkout flow for your application. It handles collecting, saving, and reusing customer payment details, and can also be used to collect shipping information. It can also serve as the underlying data source for your checkout's view controller. Most importantly, it lets you build a single integration to accept payments via both credit card and Apple Pay instead of having multiple code paths.

    To work with STPPaymentContext, you'll need to write a class that conforms to STPPaymentContextDelegate. The code samples in each of the following steps are examples—your own implementation may differ depending on the structure of your app.

    To accept payments in your app, you need to:

    1. Set up an ephemeral key
    2. Set up a customer context
    3. Handle the user's payment option
    4. Handle the user's shipping info
    5. Submit the payment

    If you find that STPPaymentContext isn't flexible enough for your needs, all of the components are also available to use individually. For more on these components, see Using Custom UI Components.

    Step 1: Set up an ephemeral key

    In order for our prebuilt UI elements to function, you'll need to provide them with 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.

    Our prebuilt UI elements also operate on the assumption that you have a single Customer object for each of your users. When you create a new user or account on your server, you can create a corresponding Customer object at the same time, even if you don't collect payment information from your users when they sign up. This ensures that your application has a matching Customer for each user.

    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 will specify the version of the Stripe API that it expects the response to come from. Your endpoint must 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():
        api_version = request.args['api_version']
        customerId = session['customerId']
        key = stripe.EphemeralKey.create(customer=customerId, api_version="2017-05-25")
        return jsonify(key)
    // This assumes that $customerId has been set appropriately from session data
    if (!isset($_POST['api_version']))
    {
        exit(http_response_code(400));
    }
    try {
        $key = \Stripe\EphemeralKey::create(
          ["customer" => $customerId],
          ["stripe_version" => $_POST['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();
      });
    });
    // Using Spark framework (http://sparkjava.com)
    post(new Route(path) {
        @Override
        public Object handle(final Request request,
                             final Response response) {
            String apiVersion = request.queryParams("api_version");
            RequestOptions requestOptions = (new RequestOptions.RequestOptionsBuilder())
                                              .setStripeVersion(apiVersion)
                                              .build();
    
            try {
                // Retrieve the customer id from your session for example
                Map<String, Object> options = new HashMap<String, Object>();
                options.put("customer", customerId);
    
                EphemeralKey key = EphemeralKey.create(options, requestOptions);
                return key.getRawJson();
            } catch (StripeException e) {
                response.status(500);
                return e;
            }
        }
    });
    // 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 need to set up a way for your iOS app to communicate with this endpoint. In your app, you should make your API client class conform to the STPCustomerEphemeralKeyProvider protocol, which defines a single method, createCustomerKeyWithAPIVersion. 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.

    func createCustomerKey(withAPIVersion apiVersion: String, completion: @escaping STPJSONResponseCompletionBlock) {
        let url = self.baseURL.appendingPathComponent("ephemeral_keys")
        Alamofire.request(url, method: .post, parameters: [
            "api_version": apiVersion
            ])
            .validate(statusCode: 200..<300)
            .responseJSON { responseJSON in
                switch responseJSON.result {
                case .success(let json):
                    completion(json as? [String: AnyObject], nil)
                case .failure(let error):
                    completion(nil, error)
                }
        }
    }
    - (void)createCustomerKeyWithAPIVersion:(NSString *)apiVersion completion:(STPJSONResponseCompletionBlock)completion {
        NSURL *url = [self.baseURL URLByAppendingPathComponent:@"ephemeral_keys"];
        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager POST:url.absoluteString
           parameters:@{@"api_version": apiVersion}
             progress:nil
              success:^(NSURLSessionDataTask *task, id responseObject) {
            completion(responseObject, nil);
        } failure:^(NSURLSessionDataTask *task, NSError *error) {
            completion(nil, error);
        }];
    }

    Step 2: Set up a customer context

    In addition to setting up your STPPaymentContextDelegate, you'll also need to initialize an STPCustomerContext. The STPCustomerContext is like the backing store for STPPaymentContext. A customer context retrieves and updates its Stripe customer using an ephemeral key obtained from your server via its key provider.

    // MyAPIClient implements STPCustomerEphemeralKeyProvider (see above)
    let customerContext = STPCustomerContext(keyProvider: MyAPIClient.sharedClient)
    // MyAPIClient implements STPCustomerEphemeralKeyProvider (see above)
    STPCustomerContext *customerContext = [[STPCustomerContext alloc] initWithKeyProvider:[MyAPIClient sharedInstance]];

    STPCustomerContext will automatically prefetch its customer once its key provider has been set. If you'd like to take advantage of preloading your customer's information, you should initialize your STPCustomerContext instance earlier, before your user enters your payment flow.

    If your current user logs out of the app and a new user logs in, be sure to either create a new instance of STPCustomerContext, or clear the cached customer using the provided clearCachedCustomer method. On your backend, be sure to create and return a new ephemeral key for the Customer object associated with the new user.

    Once you've set up your customer context, you can use it to initialize STPPaymentContext. Be sure to set your payment context's delegate and hostViewController properties (these will usually be the same object, a UIViewController instance). You should also set the payment context's paymentAmount property, which will be displayed to your user in the Apple Pay dialog (you can change this later, if the amount of the user's purchase changes).

    init() {
        self.paymentContext = STPPaymentContext(customerContext: customerContext)
        super.init(nibName: nil, bundle: nil)
        self.paymentContext.delegate = self
        self.paymentContext.hostViewController = self
        self.paymentContext.paymentAmount = 5000 // This is in cents, i.e. $50 USD
    }
    - (instancetype)init {
        self = [super initWithNibName:nil bundle:nil];
        if (self) {
           self.paymentContext = [[STPPaymentContext alloc] initWithCustomerContext:customerContext]];
           self.paymentContext.delegate = self;
           self.paymentContext.hostViewController = self;
           self.paymentContext.paymentAmount = 5000 // This in cents, i.e. $50 USD
        }
        return self;
    }

    Step 3: Handle the user's payment option

    When you'd like to give your user the chance to enter or change their payment option (say, they tapped a button on your checkout page that lets them do this), STPPaymentContext can do this for you automatically. You can specify whether you'd like the payment selector view controller to be presented modally, or pushed onto a UINavigationController stack:

    // If you prefer a modal presentation
    func choosePaymentButtonTapped() {
        self.paymentContext.presentPaymentOptionsViewController()
    }
    
    // If you prefer a navigation transition
    func choosePaymentButtonTapped() {
        self.paymentContext.pushPaymentOptionsViewController()
    }
    // If you prefer a modal presentation
    - (void)choosePaymentButtonTapped {
        [self.paymentContext presentPaymentOptionsViewController];
    }
    
    // If you prefer a navigation transition
    - (void)choosePaymentButtonTapped {
        [self.paymentContext pushPaymentOptionsViewController];
    }

    This sets up and presents an STPPaymentOptionsViewController on the payment context's hostViewController. If the user doesn't have any stored payment options, it automatically prompts them to enter one.

    - paymentContextDidChange:

    This method triggers when the content of the payment context changes, like when the user selects a new payment method or enters shipping information. This is a good place to update your UI:

    func paymentContextDidChange(_ paymentContext: STPPaymentContext) {
        self.activityIndicator.animating = paymentContext.loading
        self.paymentButton.enabled = paymentContext.selectedPaymentOption != nil
        self.paymentLabel.text = paymentContext.selectedPaymentOption?.label
        self.paymentIcon.image = paymentContext.selectedPaymentOption?.image
    }
    - (void)paymentContextDidChange:(STPPaymentContext *)paymentContext {
        self.activityIndicator.animating = paymentContext.loading;
        self.paymentButton.enabled = paymentContext.selectedPaymentOption != nil;
        self.paymentLabel.text = paymentContext.selectedPaymentOption.label;
        self.paymentIcon.image = paymentContext.selectedPaymentOption.image;
    }

    Step 4: Handle the user's shipping info

    If your user needs to enter or change their shipping address and shipping method, STPPaymentContext can do this for you automatically. STPPaymentContext will save shipping info to the Stripe customer when your user updates their information, and automatically prefill the shipping view controller for future purchases. Note that you should not rely on the shipping information stored on the Stripe customer for order fulfillment, as your user may change this information if they make multiple purchases. We recommend adding shipping information when you create a Charge or PaymentIntent object (which can also help prevent fraud), or when saving it to your own database. When presenting the shipping view controller, you can specify whether you'd like it presented modally, or pushed onto a UINavigationController stack:

    // If you prefer a modal presentation
    func shippingButtonTapped() {
        self.paymentContext.presentShippingViewController()
    }
    
    // If you prefer a navigation transition
    func shippingButtonTapped() {
        self.paymentContext.pushShippingViewController()
    }
    // If you prefer a modal presentation
    - (void)shippingButtonTapped {
        [self.paymentContext presentShippingViewController];
    }
    
    // If you prefer a navigation transition
    - (void)shippingButtonTapped {
        [self.paymentContext pushShippingViewController];
    }

    This sets up and presents an STPShippingAddressViewController on the payment context's hostViewController. Once the user enters a valid shipping address, they'll be taken to an STPShippingMethodsViewController. After they select a shipping method, both view controllers will be dismissed or popped off the hostViewController's stack.

    - paymentContext:didUpdateShippingAddress:completion:

    This method is called after your user enters a shipping address. Here, you should validate the returned address and determine the shipping methods available for that address. If the address is valid, you should call the provided completion block with a status of STPShippingStatusValid, nil for the error argument, an array of shipping methods, and a selected shipping method. If you don't need to collect a shipping method, you should just pass nil for the shipping methods and selected shipping method. If the address is invalid, you should call the completion block with a status of STPShippingStatusInvalid, an error object describing the issue with the address, and nil for the shipping methods and selected shipping method. Note that providing an error object is optional—if you omit it, the user will simply see an alert with the message "Invalid Shipping Address".

    func paymentContext(_ paymentContext: STPPaymentContext, didUpdateShippingAddress address: STPAddress, completion: @escaping STPShippingMethodsCompletionBlock) {
        let upsGround = PKShippingMethod()
        upsGround.amount = 0
        upsGround.label = "UPS Ground"
        upsGround.detail = "Arrives in 3-5 days"
        upsGround.identifier = "ups_ground"
        let fedEx = PKShippingMethod()
        fedEx.amount = 5.99
        fedEx.label = "FedEx"
        fedEx.detail = "Arrives tomorrow"
        fedEx.identifier = "fedex"
    
        if address.country == "US" {
            completion(.valid, nil, [upsGround, fedEx], upsGround)
        }
        else {
            completion(.invalid, nil, nil, nil)
        }
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext didUpdateShippingAddress:(STPAddress *)address completion:(STPShippingMethodsCompletionBlock)completion {
        PKShippingMethod *upsGround = [PKShippingMethod new];
        upsGround.amount = [NSDecimalNumber decimalNumberWithString:@"0"];
        upsGround.label = @"UPS Ground";
        upsGround.detail = @"Arrives in 3-5 days";
        upsGround.identifier = @"ups_ground";
        PKShippingMethod *fedEx = [PKShippingMethod new];
        fedEx.amount = [NSDecimalNumber decimalNumberWithString:@"5.99"];
        fedEx.label = @"FedEx";
        fedEx.detail = @"Arrives tomorrow";
        fedEx.identifier = @"fedex";
        if ([address.country isEqualToString:@"US"]) {
            completion(STPShippingStatusValid, nil, @[upsGround, fedEx], upsGround);
        }
        else {
            completion(STPShippingStatusInvalid, nil, nil, nil);
        }
    }

    Step 5: Submit the payment

    Finally, when your user taps "Buy", just call requestPayment on your payment context. It'll display any required UI (such as the Apple Pay dialog) and call the appropriate methods on its delegate as your user finishes their payment.

    func payButtonTapped() {
        self.paymentContext.requestPayment()
    }
    - (void)payButtonTapped {
        [self.paymentContext requestPayment];
    }

    - paymentContext:didCreatePaymentResult:completion:

    This method is called when the customer has successfully selected a payment method and completed their purchase. This step requires server-side and client-side actions. Follow Using Payment Intents on iOS to finish charging your customer.

    This example assumes you are using manual confirmation and that self should present any additional view controllers and implements STPAuthenticationContext. See Supporting 3D Secure Authentication on iOS to learn more about STPPaymentHandler.

    func paymentContext(_ paymentContext: STPPaymentContext,
      didCreatePaymentResult paymentResult: STPPaymentResult,
      completion: @escaping STPErrorBlock) {
    
        myAPIClient.createAndConfirmPaymentIntent(paymentResult.paymentMethod.stripeID,
                                                  amount: paymentContext.paymentAmount,
                                                  shippingAddress: paymentContext.shippingAddress,
                                                  shippingMethod: paymentContext.selectedShippingMethod) {
          (paymentIntentClientSecret: String?, error: Error?) in
            guard let clientSecret = paymentIntentClientSecret, error == nil else {
              completion(error ?? ...) // Report error from your API
            }
            STPPaymentHandler.shared().handleNextAction(forPayment: clientSecret, with: self, returnURL: nil) { (status, paymentIntent, error) in
              switch (status) {
                case .succeeded:
                  if paymentIntent.status == .requiresConfirmation { // Confirm again on the backend
                    MyAPIClient.sharedClient.confirmPaymentIntent(handledPaymentIntent) { clientSecret, error in
                      guard let clientSecret = clientSecret, error == nil else {
                          completion(error ?? ...) // Report error from your API
                          return
                      }
    
                      // Retrieve the Payment Intent and check the status for success
                      STPAPIClient.shared().retrievePaymentIntent(withClientSecret: clientSecret) { (paymentIntent, error) in
                          guard let paymentIntent = paymentIntent, error == nil else {
                              completion(error ?? ...) // Report error
                              return
                          }
                          if paymentIntent.status == .succeeded {
                              completion(nil)
                          } else {
                              completion(...) // Report failure
                          }
                      }
                  }
    
                  }
                case .canceled:
                  completion(...) // Customer canceled
                case .failed:
                  completion(error ?? ...) // Report error
              }
            }
          }
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext
    didCreatePaymentResult:(STPPaymentResult *)paymentResult
                completion:(STPErrorBlock)completion {
      [self.apiClient createAndConfirmPaymentIntentWithPaymentMethod:paymentResult.paymentMethod.stripeID
                                                              amount:paymentContext.paymentAmount
                                                     shippingAddress:paymentContext.shippingAddress
                                                      shippingMethod:paymentContext.selectedShippingMethod
                                                          completion:^(NSString * paymentIntentClientSecret, NSError * error) {
        if (paymentIntentClientSecret == nil || error != nil) {
          completion(error ?: ...); // Report error from your API
        } else {
          [[STPPaymentHandler sharedHandler] handleNextActionForPayment:paymentIntentClientSecret
                                              withAuthenticationContext:self
                                                              returnURL:nil
                                                             completion:^(STPPaymentHandlerActionStatus status, STPPaymentIntent * paymentIntent, NSError * handlError) {
                switch (status) {
                  case STPPaymentHandlerActionStatusSucceeded:
                    if (paymentIntent.status == STPPaymentIntentStatusRequiresConfirmation) { // Confirm again on the backend
                      [self.apiClient confirmPaymentIntent:paymentIntent.stripeId completion:^(NSString *confirmedClientSecret, NSError *confirmError) {
                        if (confirmedClientSecret == nil || error != nil) {
                          completion(error ?: ...); // Report error from your API
                        } else {
                          [[STPAPIClient sharedClient] retrievePaymentIntentWithClientSecret:confirmedClientSecret completion:^(STPPaymentIntent *finalPaymentIntent, NSError *finalError) {
                            if (finalError) {
                              completion(finalError)
                            } else if (finalPaymentIntent.status == STPPaymentIntentStatusSucceeded) {
                              completion(nil)
                            } else {
                              completion(...) // Report failure
                            }
                          }];
                        }
                      }];
                    } else {
                      completion(nil); //
                    }
                  case STPPaymentHandlerActionStatusCanceled:
                    completion(...) // Customer canceled
                  case STPPaymentHandlerActionStatusFailed:
                    completion(error ?: ...) // Report error
                }
          }];
        }
      }];
    }

    When this API request is finished, call the provided completion block with nil as its only argument if the call succeeded, or, if an error occurred, with that error as the argument instead.

    - paymentContext:didFinishWithStatus:error:

    This method is called after the previous method, when any auxiliary UI that has been displayed (such as the Apple Pay dialog) has been dismissed. You should inspect the returned status and show an appropriate message to your user. For example:

    func paymentContext(_ paymentContext: STPPaymentContext,
      didFinishWithStatus status: STPPaymentStatus,
      error: Error?) {
    
        switch status {
        case .error:
            self.showError(error)
        case .success:
            self.showReceipt()
        case .userCancellation:
            return // Do nothing
        }
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext
       didFinishWithStatus:(STPPaymentStatus)status
                     error:(NSError *)error {
        switch (status) {
            case STPPaymentStatusSuccess:
                [self showReceipt];
            case STPPaymentStatusError:
                [self showError:error];
            case STPPaymentStatusUserCancellation:
                return; // Do nothing
        }
    }

    - paymentContext:didFailToLoadWithError:

    This method is called in the rare case that the payment context's initial loading call fails, usually due to lack of internet connectivity. You should dismiss your checkout page when this occurs and invite the user to try again. You can also optionally attempt to try again by calling retryLoading on the payment context.

    func paymentContext(_ paymentContext: STPPaymentContext,
      didFailToLoadWithError error: Error) {
        self.navigationController?.popViewController(animated: true)
        // Show the error to your user, etc.
    }
    - (void)paymentContext:(STPPaymentContext *)paymentContext didFailToLoadWithError:(NSError *)error {
         [self.navigationController popViewControllerAnimated:YES];
         // Show the error to your user, etc.
    }

    Customizing the look-and-feel of Stripe UI elements

    We built the payment selector so that you can define your own appearance and apply the attributes you want to customize from the default theme that we provide.

    Stripe iOS UI Theming

    You can override any of the properties from the default theme, typically in your AppDelegate:

    STPTheme.default().accentColor = UIColor.blue
    [[STPTheme defaultTheme] setAccentColor:[UIColor blueColor]];

    Here is a list of properties you can customize and their respective usage inside the payment selector UI:

    Property Name Description
    primaryBackgroundColor Background color for any views in this theme
    secondaryBackgroundColor Background color for any supplemental views inside a view (for example the cells of a table view)
    primaryForegroundColor Text color for any important labels in a view
    secondaryForegroundColor Text color for any supplementary labels in a view
    accentColor Color for any buttons and other elements on a view that are important to highlight
    errorColor Color for rendering any error messages or views
    font Font to be used for all views
    emphasisFont Medium-weight font to be used for all bold text in views

    We also let you customize all the fonts used in this payment selector flow. By default, the SDK will use the system font at different weight and sizes. To override the defaults, set the font and emphasisFont properties and the SDK will automatically use your choice instead and infer the rest.

    Next steps

    Was this page helpful? Yes No

    Send

    Thank you for helping improve Stripe's documentation. If you need help or have any questions, please consider contacting support.

    Questions?

    We're always happy to help with code or other questions you might have. Search our documentation, contact support, or connect with our sales team. You can also chat live with other developers in #stripe on freenode.

    On this page