Skip to main content
โšก Calmops

Mobile Payment Integration: Stripe, Apple Pay & Google Pay

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