Back to Blog
iOSSwiftUIStripePaymentsMobile

Implementing Stripe Tap to Pay in SwiftUI: A Production Guide

A comprehensive guide to implementing Stripe's Tap to Pay on iPhone in a production SwiftUI app, based on our real-world deployment for Prima Bilvård.

Retro87 Team
January 15, 2025

Implementing Stripe Tap to Pay in SwiftUI: A Production Guide

Stripe's Tap to Pay on iPhone is a game-changer for businesses looking to accept contactless payments without dedicated hardware. We recently deployed this in production for Prima Bilvård, turning staff iPhones into payment terminals and eliminating hardware costs entirely.

Why Tap to Pay on iPhone?

Traditional POS systems require expensive hardware, maintenance, and inventory management. Tap to Pay on iPhone transforms any compatible iPhone into a payment terminal, accepting contactless cards and digital wallets with just a phone.

Key Benefits

  • Zero Hardware Costs: No need to purchase, maintain, or inventory POS terminals
  • Instant Deployment: Staff can accept payments immediately on their existing iPhones
  • Security Built-In: PCI-DSS compliant, leveraging iPhone's secure element
  • Flexible: Accept payments anywhere - in-store, on-site, or mobile

Technical Requirements

Before diving into implementation, ensure you have:

  • iOS 15.4+ (Tap to Pay requires newer iOS versions)
  • iPhone XS or later (requires NFC hardware)
  • Stripe account with Tap to Pay enabled
  • Active Apple Developer Program membership

Architecture Overview

Our implementation consists of three main components:

  1. SwiftUI Payment Flow - User interface for initiating and completing transactions
  2. Stripe SDK Integration - Communication with Stripe's payment APIs
  3. Backend API - Payment intent creation and verification

Step 1: Setting Up the Stripe SDK

First, add the Stripe iOS SDK to your project via Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/stripe/stripe-ios", from: "23.0.0")
]

Step 2: Initializing the Terminal

Create a service to manage the Stripe Terminal connection:

import StripeTerminal

class PaymentService: ObservableObject {
    @Published var isReaderConnected = false
    
    func initializeTerminal() {
        StripeTerminal.setTokenProvider(APITokenProvider())
        
        let config = LocalMobileReaderConfiguration()
        Terminal.shared.discoverReaders(config) { result in
            switch result {
            case .success(let readers):
                self.connectToReader(readers.first)
            case .failure(let error):
                print("Discovery failed: \\(error)")
            }
        }
    }
}

Step 3: Creating Payment Intents

Your backend needs to create payment intents:

// Backend API (Node.js example)
app.post('/create-payment-intent', async (req, res) => {
  const { amount, currency } = req.body;
  
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency,
    payment_method_types: ['card_present'],
    capture_method: 'automatic',
  });
  
  res.json({ clientSecret: paymentIntent.client_secret });
});

Step 4: Building the SwiftUI Payment Interface

Create a clean, user-friendly payment view:

struct TapToPayView: View {
    @StateObject private var paymentService = PaymentService()
    @State private var amount: String = ""
    @State private var isProcessing = false
    
    var body: some View {
        VStack(spacing: 24) {
            Text("Tap to Pay")
                .font(.largeTitle)
                .fontWeight(.bold)
            
            TextField("Amount", text: $amount)
                .keyboardType(.decimalPad)
                .textFieldStyle(.roundedBorder)
            
            Button("Process Payment") {
                processPayment()
            }
            .disabled(isProcessing || amount.isEmpty)
            
            if isProcessing {
                ProgressView("Processing...")
            }
        }
        .padding()
    }
}

Step 5: Processing Payments

Handle the payment collection flow:

func processPayment() {
    isProcessing = true
    
    // Create payment intent on backend
    createPaymentIntent(amount: amountInCents) { clientSecret in
        // Collect payment with Tap to Pay
        Terminal.shared.collectPaymentMethod(clientSecret) { result in
            switch result {
            case .success(let paymentIntent):
                confirmPayment(paymentIntent)
            case .failure(let error):
                handleError(error)
            }
        }
    }
}

Production Lessons Learned

1. Error Handling is Critical

Network issues, card declines, and user cancellations happen. Implement robust error handling:

enum PaymentError: Error {
    case readerNotConnected
    case networkError
    case cardDeclined
    case userCancelled
}

2. Offline Support

Implement offline transaction queuing for areas with poor connectivity:

class OfflinePaymentQueue {
    func queuePayment(_ payment: PendingPayment) {
        // Store locally, sync when online
    }
}

3. GDPR Compliance

Ensure all payment data handling is GDPR compliant:

  • Never log full card numbers
  • Encrypt sensitive data at rest
  • Implement data retention policies
  • Provide customer data export functionality

4. Testing Strategy

Test thoroughly with Stripe's test cards:

  • Successful payments
  • Declined cards
  • Network timeouts
  • Reader disconnections

Real-World Performance

In our Prima Bilvård deployment:

  • 99.8% payment success rate
  • Average transaction time: 8 seconds
  • Zero hardware maintenance costs
  • Staff onboarded in < 5 minutes

Security Considerations

Tap to Pay on iPhone is PCI-DSS compliant, but you still need to:

  1. Never store card data - Let Stripe handle tokenization
  2. Use HTTPS - All backend communication must be encrypted
  3. Validate on backend - Never trust client-side payment confirmations
  4. Monitor for fraud - Implement Stripe Radar

Conclusion

Stripe Tap to Pay on iPhone eliminates hardware costs while providing a secure, compliant payment solution. Our production deployment for Prima Bilvård proves it scales for real-world business operations.

The key to success is robust error handling, thorough testing, and security-first architecture. With proper implementation, you can turn any iPhone into a powerful payment terminal.


Need help implementing Tap to Pay for your business? Get in touch and let's discuss your requirements.

Need help with your project?

We solve complex technical challenges in production. Let's discuss how we can help.

Start a Conversation