Mobile Payment Integration: Stripe, Apple Pay & Google Pay
TL;DR: This guide covers payment integration for mobile apps including Stripe SDK, Apple Pay, Google Pay, subscriptions, and security best practices for handling payments.
Introduction
Integrating payments into mobile apps requires careful consideration of security, user experience, and platform-specific requirements. This guide covers the major payment providers and implementation patterns.
Payment Provider Comparison
| Provider | Fees | Platforms | Key Features |
|---|---|---|---|
| Stripe | 2.9% + $0.30 | iOS, Android | Subscriptions, marketplace |
| Apple Pay | 0.5% - 1.5% | iOS only | Fast checkout, biometric |
| Google Pay | 0.5% - 1.5% | Android only | Fast checkout, tokens |
| In-App Purchases | 15% - 30% | iOS, Android | Auto-renewing subscriptions |
Stripe Integration
iOS - Swift
Installing Stripe SDK:
# Podfile
pod 'StripePaymentSheet', '~> 23.0'
pod 'StripeCore', '~> 23.0'
Payment View Controller:
import UIKit
import StripePaymentSheet
class PaymentViewController: UIViewController {
private var paymentSheet: PaymentSheet?
private let backendURL = "https://api.yourapp.com"
override func viewDidLoad() {
super.viewDidLoad()
setupPaymentSheet()
}
private func setupPaymentSheet() {
// Fetch payment intent from your backend
fetchPaymentIntent { [weak self] result in
switch result {
case .success(let (clientSecret, ephemeralKey, customerId)):
self?.configurePaymentSheet(
clientSecret: clientSecret,
ephemeralKey: ephemeralKey,
customerId: customerId
)
case .failure(let error):
print("Error: \(error)")
}
}
}
private func configurePaymentSheet(
clientSecret: String,
ephemeralKey: String,
customerId: String
) {
var configuration = PaymentSheet.Configuration()
configuration.merchantDisplayName = "My App"
configuration.customer = .init(
id: customerId,
ephemeralKeySecret: ephemeralKey
)
configuration.applePay = .init(
merchantId: "merchant.com.yourapp",
merchantCountryCode: "US"
)
paymentSheet = PaymentSheet(
paymentIntentClientSecret: clientSecret,
configuration: configuration
)
}
@IBAction func presentPaymentSheet() {
paymentSheet?.present(from: self) { [weak self] result in
switch result {
case .completed:
self?.showSuccessAlert()
case .canceled:
print("Payment canceled")
case .failed(let error):
self?.showErrorAlert(error: error)
}
}
}
private func fetchPaymentIntent(
completion: @escaping (Result<(String, String, String), Error>) -> Void
) {
guard let url = URL(string: "\(backendURL)/create-payment-intent") else {
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: Any] = [
"amount": 2999,
"currency": "usd",
"customerId": "cus_xxx"
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data,
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let clientSecret = json["clientSecret"] as? String,
let ephemeralKey = json["ephemeralKey"] as? String,
let customerId = json["customerId"] as? String else {
completion(.failure(NSError(domain: "", code: -1)))
return
}
completion(.success((clientSecret, ephemeralKey, customerId)))
}.resume()
}
}
Android - Kotlin
Installing Stripe SDK:
// build.gradle
dependencies {
implementation 'com.stripe:stripe-android:20.22.0'
}
Payment Activity:
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.stripe.android.PaymentConfiguration
import com.stripe.android.paymentelement.PaymentElement
import com.stripe.android.paymentelement.PaymentSheet
import com.stripe.android.paymentelement.rememberPaymentSheet
class PaymentActivity : AppCompatActivity() {
private lateinit var paymentSheet: PaymentSheet
private lateinit var paymentElement: PaymentElement
private val backendUrl = "https://api.yourapp.com"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_payment)
PaymentConfiguration.init(
applicationContext,
"pk_test_xxx"
)
paymentSheet = rememberPaymentSheet { sheet ->
sheet.present(
paymentIntentClientSecret = "{PI_CLIENT_SECRET}",
configuration = PaymentSheet.Configuration(
merchantDisplayName = "My App",
allowsDelayedPaymentMethods = true
)
).observe { result ->
when (result) {
PaymentSheet.Result.Completed -> showSuccess()
PaymentSheet.Result.Canceled -> showCanceled()
is PaymentSheet.Result.Failed -> showError(result.error)
}
}
}
fetchPaymentIntent()
}
private fun fetchPaymentIntent() {
val request = Request.Builder()
.url("$backendUrl/create-payment-intent")
.post(
FormBody.Builder()
.add("amount", "2999")
.add("currency", "usd")
.build()
)
.build()
OkHttpClient().newCall(request).enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val json = JSONObject(response.body?.string() ?: "")
val clientSecret = json.getString("clientSecret")
runOnUiThread { displayPaymentElement(clientSecret) }
}
override fun onFailure(call: Call, e: IOException) = Unit
})
}
private fun displayPaymentElement(clientSecret: String) {
val paymentElement = PaymentElement(
context = this,
paymentIntentClientSecret = clientSecret,
configuration = PaymentSheet.Configuration(
merchantDisplayName = "My App"
)
)
findViewById<FrameLayout>(R.id.paymentElementContainer)
.addView(paymentElement)
}
@Suppress("UNUSED_PARAMETER")
private fun onPayClicked(view: View) {
paymentSheet.present()
}
}
Apple Pay Integration
iOS - Swift
Setting up Apple Pay:
import PassKit
class ApplePayHandler: NSObject {
static let supportedNetworks: [PKPaymentNetwork] = [
.visa, .masterCard, .amex, .discover
]
static func canMakePayments() -> Bool {
PKPaymentAuthorizationViewController.canMakePayments()
}
static func createPaymentRequest(
amount: Decimal,
merchantId: String
) -> PKPaymentRequest {
let request = PKPaymentRequest()
request.merchantIdentifier = merchantId
request.supportedNetworks = supportedNetworks
request.merchantCapabilities = .threeDSecure
request.countryCode = "US"
request.currencyCode = "USD"
let total = PKPaymentSummaryItem(
label: "My App Purchase",
amount: NSDecimalNumber(decimal: amount)
)
request.paymentSummaryItems = [total]
return request
}
static func presentPaymentSheet(
from viewController: UIViewController,
amount: Decimal,
merchantId: String,
completion: @escaping (Result<PKPaymentToken, Error>) -> Void
) {
let request = createPaymentRequest(amount: amount, merchantId: merchantId)
guard let paymentVC = PKPaymentAuthorizationViewController(paymentRequest: request) else {
completion(.failure(ApplePayError.notAvailable))
return
}
let handler = ApplePayHandler()
handler.completion = completion
paymentVC.delegate = handler
viewController.present(paymentVC, animated: true)
}
var completion: ((Result<PKPaymentToken, Error>) -> Void)?
}
extension ApplePayHandler: PKPaymentAuthorizationViewControllerDelegate {
func paymentAuthorizationViewController(
_ controller: PKPaymentAuthorizationViewController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
let tokenData = payment.token.paymentData
let tokenString = payment.token.paymentData.base64EncodedString()
// Send token to your server for processing
sendTokenToServer(tokenString) { result in
switch result {
case .success:
completion(PKPaymentAuthorizationResult(status: .success, errors: nil))
case .failure(let error):
completion(PKPaymentAuthorizationResult(status: .failure, errors: [error]))
}
}
}
func paymentAuthorizationViewControllerDidFinish(
_ controller: PKPaymentAuthorizationViewController
) {
controller.dismiss(animated: true)
}
private func sendTokenToServer(
_ token: String,
completion: @escaping (Result<Void, Error>) -> Void
) {
// Implementation
}
}
enum ApplePayError: Error {
case notAvailable
}
Google Pay Integration
Android - Kotlin
Setting up Google Pay:
import android.app.Activity
import com.google.android.gms.wallet.*
class GooglePayHandler(private val activity: Activity) {
private val paymentsClient = Wallet.getPaymentsClient(
activity,
WalletOptions.Builder()
.setEnvironment(WalletConstants.ENVIRONMENT_TEST)
.build()
)
private val baseRequest = JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
}
private fun getGatewayTokenization(): JSONObject {
return JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put("parameters", JSONObject().apply {
put("gateway", "stripe")
put("gatewayMerchantId", "pk_test_xxx")
})
}
}
private fun getCardPaymentMethod(): JSONObject {
return JSONObject().apply {
put("type", "CARD")
put("parameters", JSONObject().apply {
put("allowedAuthMethods", JSONArray().apply {
put("PAN_ONLY")
put("CRYPTOGRAM_3DS")
})
put("allowedCardNetworks", JSONArray().apply {
put("VISA")
put("MASTERCARD")
put("AMEX")
})
})
put("tokenizationSpecification", getGatewayTokenization())
}
}
fun isReadyToPayRequest(): JSONObject {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(getCardPaymentMethod()))
}
}
fun createPaymentDataRequest(price: String): JSONObject {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(getCardPaymentMethod()))
put("transactionInfo", JSONObject().apply {
put("totalPrice", price)
put("totalPriceStatus", "FINAL")
put("currencyCode", "USD")
})
put("merchantName", "My App")
}
}
fun requestPayment(
price: String,
onSuccess: (String) -> Unit,
onError: (Exception) -> Unit
) {
val request = createPaymentDataRequest(price)
AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(request),
activity,
LOAD_PAYMENT_DATA_REQUEST_CODE
)
}
companion object {
private const val LOAD_PAYMENT_DATA_REQUEST_CODE = 123
}
}
Handling the result:
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
when (resultCode) {
Activity.RESULT_OK -> {
val paymentData = data?.getStringExtra(
PaymentIntentConstants.EXTRA_PAYMENT_DATA
)
// Send paymentData to your server
}
Activity.RESULT_CANCELED -> {
// User canceled
}
AutoResolveHelper.RESULT_ERROR -> {
val status = AutoResolveHelper.getStatusFromIntent(data)
// Handle error
}
}
}
}
}
Subscriptions
iOS - StoreKit 2
import StoreKit
@MainActor
class SubscriptionManager: ObservableObject {
@Published var products: [Product] = []
@Published var purchasedProductIDs: Set<String> = []
init() {
Task {
await loadProducts()
await updatePurchasedProducts()
}
}
func loadProducts() async {
do {
let productIDs = Set(["premium_monthly", "premium_yearly"])
products = try await Product.products(for: productIDs)
} catch {
print("Failed to load products: \(error)")
}
}
func purchase(_ product: Product) async throws -> Bool {
let result = try await product.purchase()
switch result {
case .success(let verification):
let transaction = try checkVerified(verification)
await updatePurchasedProducts()
await transaction.finish()
return true
case .userCancelled:
return false
case .pending:
return false
@unknown default:
return false
}
}
func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
if case .verified(let transaction) = result {
purchasedProductIDs.insert(transaction.productID)
}
}
}
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .verified(let safe):
return safe
case .unverified:
throw VerificationError.failed
}
}
}
enum VerificationError: Error {
case failed
}
Android - Billing Library
import com.android.billingclient.api.*
class SubscriptionManager(private val context: Context) {
private val billingClient = BillingClient.Builder(context)
.setListener { result, purchases ->
if (result.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
for (purchase in purchases) {
handlePurchase(purchase)
}
}
}
.build()
private val productIds = listOf("premium_monthly", "premium_yearly")
fun startConnection(onConnected: () -> Unit) {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
onConnected()
}
}
override fun onBillingServiceDisconnected() {
// Handle disconnection
}
})
}
fun querySubscriptions() {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
billingClient.queryPurchasesAsync(params.build()) { result, purchases ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
for (purchase in purchases) {
handlePurchase(purchase)
}
}
}
}
fun purchaseSubscription(activity: Activity, productId: String) {
val params = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(
listOf(
ProductDetailsParams.newBuilder()
.setProductId(productId)
.setOfferToken("offer_token")
.build()
)
)
.build()
billingClient.launchBillingFlow(activity, params)
}
private fun handlePurchase(purchase: Purchase) {
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchase.isAcknowledged) {
acknowledgePurchase(purchase)
}
}
}
private fun acknowledgePurchase(purchase: Purchase) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { result ->
if (result.responseCode == BillingClient.BillingResponseCode.OK) {
// Handle success
}
}
}
}
Security Best Practices
Do
- Never store payment credentials on device
- Use tokenization provided by payment providers
- Implement server-side payment processing
- Use SSL/TLS for all communications
- Implement fraud detection
- Comply with PCI-DSS requirements
Don’t
- Log sensitive payment data
- Store card numbers locally
- Process payments on client-side only
- Use hardcoded API keys
- Trust client-side validation alone
Server-Side Implementation
Stripe - Payment Intent Creation
import stripe
from flask import Flask, request, jsonify
stripe.api_key = "sk_test_xxx"
app = Flask(__name__)
@app.route("/create-payment-intent", methods=["POST"])
def create_payment_intent():
data = request.json
amount = data.get("amount", 0)
currency = data.get("currency", "usd")
# Create or get customer
customer = stripe.Customer.create()
# Create payment intent
payment_intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
customer=customer.id,
automatic_payment_methods={"enabled": True},
)
# Create ephemeral key
ephemeral_key = stripe.EphemeralKey.create(
customer=customer.id,
stripe_version="2023-10-16"
)
return jsonify({
"clientSecret": payment_intent.client_secret,
"ephemeralKey": ephemeral_key.secret,
"customerId": customer.id
})
@app.route("/webhook", methods=["POST"])
def webhook():
payload = request.data
sig_header = request.headers.get("Stripe-Signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, "whsec_xxx"
)
except ValueError:
return "Invalid payload", 400
except stripe.error.SignatureVerificationError:
return "Invalid signature", 400
if event.type == "payment_intent.succeeded":
payment_intent = event.data.object
# Fulfill the purchase
fulfill_order(payment_intent.id)
return "", 200
Error Handling
// iOS - Handle common errors
enum PaymentError: Error {
case invalidCard
case declined
case networkError
case cancelled
var localizedDescription: String {
switch self {
case .invalidCard: return "Invalid card details"
case .declined: return "Card was declined"
case .networkError: return "Network error occurred"
case .cancelled: return "Payment was cancelled"
}
}
}
func handlePaymentError(_ error: Error) -> PaymentError {
if let stripeError = error as? StripeError {
switch stripeError {
case .cardDeclined:
return .declined
case .invalidCard:
return .invalidCard
default:
return .networkError
}
}
return .networkError
}
Conclusion
Mobile payment integration requires careful implementation of security, user experience, and platform-specific requirements. Use established payment providers like Stripe, Apple Pay, and Google Pay to ensure compliance and security.
Comments