Processing payments using Stripe SDK iOS

Processing payments using Stripe SDK iOS

This guide explains how to install and set up Stripe SDK on your iOS application.

Aim

Open the inbuilt Address and Payment Sheet of Stripe and start accepting payments from your customer.

Minimum iOS Deployment Version

12.0

Stripe Frameworks

  1. Stripe (22.8.4)

  2. StripeApplePay (22.8.4)

  3. StripeCore (22.8.4)

  4. StripeUICore (22.8.4)

Requirements

  1. MacBook / MacOS

  2. CocoaPods installed on your system

  3. Xcode

  4. Terminal

Proof of Concept

iOS project and test backend for processing payments with Stripe SDK.

GitHub - shivammaggu/payments-using-stripe

Setup

Adding the Stripe dependency

  1. Open Terminal in the project directory and do pod init.

  2. Open the newly created Podfile and set the iOS version to 12.0.

     # Uncomment the next line to define a global platform for your project
     platform :ios, '12.0'
    
  3. Add the Stripe dependency to the Podfile.

     target 'payments with stripe' do
       # Comment the next line if you don't want to use dynamic frameworks
       use_frameworks!
    
       # Pods for payments with stripe
    
       pod 'Stripe'
    
     end
    
  4. Save the Podfile and do a pod install on the Terminal. The following dependencies showed be installed.

    pod install

    Analyzing dependencies
    Downloading dependencies
    Installing Stripe (22.8.4)
    Installing StripeApplePay (22.8.4)
    Installing StripeCore (22.8.4)
    Installing StripeUICore (22.8.4)
    Generating Pods project
    Integrating client project

  5. Close the payments with stripe.xcodeproj and open payments with stripe.xcworkspace.

  6. Select a simulator and run the project. The project should run successfully with a blank screen.

Getting started with Stripe Developers Dashboard

This process should be done once. All the teams will use the same Dashboard. (Backend, iOS, Android etc.)

  1. Go to stripe.com and click on start now.

  2. Follow the steps as directed by the website to complete the registration process.

  3. Complete the email verification and set up your business details for which payments will be accepted by Stripe.

  4. On the Stripe Dashboard, you should be able to see your Publishable key and Secret key for both test mode and live mode.

    1. The publishable key is used on the client side such as in iOS, android or web apps.

    2. The secret key is used on the server side. (Secret key should not be stored on client-facing apps.)

Setup Stripe in your iOS App

  • Open AppDelegate.swift and add the Publishable key inside the didFinishLaunchingWithOptions method. The publishable key can be retrieved from the Stripe Developers Dashboard.

      StripeAPI.defaultPublishableKey = "Insert your key here"
    

    Use the test mode keys during the testing and development phase, and the live mode keys when publishing the app.

  • We need to hit a POST API to create the payment intent and in return receive the following data. This data is used to initialize the customer, Stripe SDK and the PaymentSheet.

    1. customerId

    2. ephemeralKey

    3. paymentIntent

    4. publishableKey

Refer to CourseCheckoutViewModel in the Proof of Concept example on GitHub.

// Creates a payment intent for the customer and fetches the important keys and id's for processing a payment

func fetchPaymentIntent(completion: @escaping ((Bool, String?) -> Void)) {

    // This could be the details for the item that the customer wants to buy

    let cartContent: [String: Any] = ["items": [["id": UUID().uuidString]]]

    self.apiClient.createPaymentIntent(cartContent: cartContent) { [weak self] (data, error) in

        guard let self = self else { return }

        guard error == nil else {
            print(error.debugDescription)
            completion(false, error.debugDescription)
            return
        }

        guard
            let customerId = data?.customerId as? String,
            let customerEphemeralKeySecret = data?.ephemeralKey as? String,
            let paymentIntentClientSecret = data?.paymentIntent as? String,
            let publishableKey = data?.publishableKey as? String
        else {
            let error = "Error fetching required data"
            print(error)
            completion(false, error)
            return
        }

        print("Created Payment Intent")

        self.setPublishableKey(publishableKey: publishableKey)
        self.paymentIntentClientSecret = paymentIntentClientSecret
        self.customer = .init(id: customerId,
                              ephemeralKeySecret: customerEphemeralKeySecret)
        completion(true, nil)
    }
}
  • Setup the PaymentSheet in your View Controller. Refer to CourseCheckoutViewController in the Proof of Concept example on GitHub.
// Create and configure the payment sheet

private func createPaymentSheet() {
    var configuration = PaymentSheet.Configuration()

    // Autofills the shipping details with the one we collected from user through AddressViewController

    configuration.shippingDetails = { [weak self] in
        return self?.address
    }

    // Configure button for Apple Pay in PaymentSheet

    configuration.applePay = .init(merchantId: "com.example.appname",
                                    merchantCountryCode: "US")

    // Provides checkbox for saving card details

    configuration.savePaymentMethodOptInBehavior = .requiresOptIn

    // Sets the return url. Used when user needs to go outside the app for authentication

    configuration.returnURL = "payments-with-stripe://stripe-redirect"

    // Configures the color of the pay button

    configuration.primaryButtonColor = .blue

    // Shows the name of the company collecting the payment

    configuration.merchantDisplayName = "Dummy Corp Inc"

    // Allows payments which requires some time to confirm

    configuration.allowsDelayedPaymentMethods = true


    // Adds default billing details for user to the payment object

    configuration.defaultBillingDetails.email = "dummy@corp.com"
    configuration.defaultBillingDetails.name = self.address?.name
    configuration.defaultBillingDetails.phone = self.address?.phone
    configuration.defaultBillingDetails.address.country = self.address?.address.country
    configuration.defaultBillingDetails.address.city = self.address?.address.city
    configuration.defaultBillingDetails.address.line1 = self.address?.address.line1
    configuration.defaultBillingDetails.address.line2 = self.address?.address.line2
    configuration.defaultBillingDetails.address.state = self.address?.address.state
    configuration.defaultBillingDetails.address.postalCode = self.address?.address.postalCode

    // Sets customer object to payment object

    if let customer = self.viewModel.getCustomer() {
        configuration.customer = customer
    }

    // Initiaises the PaymentSheet with the above config and client secret received from payment intent API

    if let paymentIntentClientSecret = self.viewModel.getPaymentIntentClientSecret() {
        self.paymentSheet = PaymentSheet(paymentIntentClientSecret: paymentIntentClientSecret,
                                        configuration: configuration)
    }
}
  • Handle the conditions after the payment. The following three scenarios can happen during/after the payment.
// Present user with the payment sheet and handles the various payment scenarios

private func presentPaymentSheet() {
    guard let paymentSheet = paymentSheet else { return }

    DispatchQueue.main.async {
        paymentSheet.present(from: self) { [weak self] (paymentResult) in

            guard let self = self else { return }

            switch paymentResult {
            case .completed:
                self.displayAlert(title: "Payment complete!")
            case .canceled:
                self.displayAlert(title: "Payment canceled!")
            case .failed(let error):
                self.displayAlert(title: "Payment failed", message: error.localizedDescription)
            }
        }
    }
}
  • On tap of the pay now button, present the payment sheet, add the payment details and click on pay.

  • Either the payment will be completed or it will fail if there are issues with the payment method. We can debug the failure reasons in the following link.

  • Payment can also result in cancellation if the user dismisses the PaymentSheet.

Address collection

  1. We can set up the AddressViewController to display a form for collecting names, addresses, cities, countries, postal codes and phone numbers.

     // Setup and display the address sheet
    
     private func presentAddressCollectionController(_ controller: CourseCheckoutViewController) {
         let addressConfiguration = AddressViewController.Configuration(additionalFields: .init(name: .required,
                                                                                                phone: .required),
                                                                         allowedCountries: [],
                                                                         title: "Billing Address")
         let addressViewController = AddressViewController(configuration: addressConfiguration,
                                                           delegate: controller)
    
         let navigationController = UINavigationController(rootViewController: addressViewController)
         self.rootViewController.present(navigationController, animated: true)
     }
    
  2. Set up the delegate method to get the address details filled in by the customer.

     extension CourseCheckoutViewController: AddressViewControllerDelegate {
    
         func addressViewControllerDidFinish(_ addressViewController: Stripe.AddressViewController, with address: Stripe.AddressViewController.AddressDetails?) {
             addressViewController.dismiss(animated: true) {
                 debugPrint(address as Any)
    
                 self.address = address
             }
         }
     }
    
  3. The address details collected from the user should be passed to PaymentSheet Configuration. This will auto-fill the details in the PaymentSheet and also send the customer details to Stripe Dashboard.

  4. Collecting these details is essential if the Stripe account is registered in India but the users are paying in international currency. More on this here.

     var configuration = PaymentSheet.Configuration()
    
     configuration.shippingDetails = { [weak self] in
         return self?.address
     }
    

Enable card scanning

To enable the option for scanning debit/credit cards, add the following key to the info.plist file.

  <key>NSCameraUsageDescription</key>
  <string>Allow the app to scan cards.</string>

This feature is available on iOS 13.0 and above.

Process payments using Apple Pay

Steps for integrating Apple Pay are well documented in the links below.

Please check out this tutorial for adding Apple Pay as a payment option to your app.

  1. Apple Pay with Payment Sheet

  2. Setup Apple Pay button

Add an Apple pay button on your PaymentSheet by editing the configuration as follows.

var configuration = PaymentSheet.Configuration()
configuration.applePay = .init(merchantId: "merchant.com.your_app_name",
                               merchantCountryCode: "US")

Return to the app automatically after external authentication

A customer may go out of your app for authentication purposes, for example, in Safari or their banking app to complete payment.

To allow them to automatically return to your app after authenticating, configure a custom URL scheme or universal link and set up your app delegate/scene delegate

(If using scene delegate for iOS 13 and above) to forward the URL to the SDK.

  • AppDelegate.swift

      // This method handles opening custom URL schemes
      // (for example, "your-app://stripe-redirect")
    
      func application(_ app: UIApplication,
                        open url: URL, 
                        options: [UIApplication.OpenURLOptionsKey : Any] = [:]
      ) -> Bool {
    
          let stripeHandled = StripeAPI.handleURLCallback(with: url)
    
          if !stripeHandled {
    
              /*
                This was not a stripe url, do whatever url handling your app
                normally does, if any.
                */
          }
    
          return true
      }
    
      // This method handles opening universal link URLs
      // (for example, "https://example.com/stripe_ios_callback")
    
      func application(_ application: UIApplication,
                        continue userActivity: NSUserActivity,
                        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
      ) -> Bool  {
    
          if userActivity.activityType == NSUserActivityTypeBrowsingWeb {
    
              if let url = userActivity.webpageURL {
    
                  let stripeHandled = StripeAPI.handleURLCallback(with: url)
    
                  guard !stripeHandled else { return true }
    
                  /*
                    This was not a stripe url, do whatever url handling your app
                    normally does, if any.
                    */
              }
          }
    
          return false
      }
    
  • SceneDelegate.swift

      func scene(_ scene: UIScene,
                  openURLContexts URLContexts: Set<UIOpenURLContext>) {
    
          if let urlContext = URLContexts.first {
    
              let url = urlContext.url
              let stripeHandled = StripeAPI.handleURLCallback(with: url)
    
              if !stripeHandled {
                  // This was not a stripe url, do whatever url handling your app
                  // normally does, if any.
              }
          }
      }
    

Also, a returnURL should be configured in the PaymentSheet.Configuration object to the URL for your app.

var configuration = PaymentSheet.Configuration()
configuration.returnURL = "payments-with-stripe://stripe-redirect"

Clear cookies on logout

PaymentSheet related cookies and other authentication data should be cleared when a user logs out of the app. Add the following code to your logout function.

import Stripe

// Resets all payment and user related cache
// Should be called when user logs out of the app

func logout() {

    /*
      Perform all other logout related steps here
      */

    PaymentSheet.resetCustomer()
}

FAQ

  1. Stripe SDK can be installed using CocoaPods, Carthage, and Swift Package Manager.

  2. The latest version v23.3.2 or above of Stripe iOS SDK requires Xcode 13.2.1 or later and is compatible with apps targeting iOS 13 or above.

  3. For iOS 12 support use → v22.8.4.

  4. For iOS 11 support use → v21.13.0.

  5. For iOS 10 support use → v19.4.0.

  6. For iOS 9 support use → v17.0.2.

  7. Why was my card declined when trying to make a payment? Check the reason for your decline code.

  8. Got an error message while processing your payment? Check the reason in this article.

Resources

  1. Stripe SDK on CocoaPods

  2. Stripe SDK on GitHub

  3. Stripe API Reference

  4. Stripe Documentation

  5. Stripe iOS SDK Documentation

  6. Installing Stripe SDK using CocoaPods

  7. About API Keys

  8. Test mode and Live mode

  9. Accepting Payments with Stripe

  10. Collecting User's Address

  11. Dummy Credentials for testing payments

  12. Accept payment from regions outside of India

  13. Adding multiple payment methods

  14. Debug card Decline Codes

  15. Understanding payment Error Codes

  16. Collecting user phone numbers through the backend

  17. Adding additional articles on security when processing payments using Stripe:

    1. Strong Customer Authentication

    2. Security

    3. 3-D Secure

    4. Privacy Details for iOS