Mobile App Integration: Accepting Crypto Payments in iOS and Android Apps

Mobile apps are increasingly adopting cryptocurrency payments. Whether you're building an e-commerce app, a SaaS mobile client, or a gaming platform, integrating crypto payments can expand your user base and reduce transaction costs. This guide covers everything you need to integrate crypto payments into iOS and Android apps.

Why Crypto Payments in Mobile Apps?

Advantages

  • Global Reach: Accept payments from users worldwide
  • Lower Fees: Reduce payment processing costs
  • Faster Settlement: Receive funds in minutes
  • No App Store Fees: Avoid 30% commission on in-app purchases (for digital goods)
  • User Privacy: Customers don't need to share credit card details
  • 24/7 Availability: Process payments anytime

Architecture Overview

Recommended Architecture

Mobile App (iOS/Android) ↓ Backend API (Your Server) ↓ Payment Gateway API (FromChain) ↓ Blockchain Network (BSC)

Why this architecture?

  • API keys stay secure on your backend
  • Mobile apps don't handle sensitive credentials
  • Centralized payment logic
  • Easier to update and maintain

iOS Integration (Swift)

Step 1: Create Payment Service

import Foundation

class PaymentService {
    private let baseURL = "https://api.fromchain.plus/v1"
    private let apiKey: String
    
    init(apiKey: String) {
        self.apiKey = apiKey
    }
    
    func createInvoice(amount: String, description: String) async throws -> Invoice {
        guard let url = URL(string: "\(baseURL)/invoices") else {
            throw PaymentError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let body: [String: Any] = [
            "amount": amount,
            "currency": "USDT",
            "description": description
        ]
        
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        
        let (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw PaymentError.requestFailed
        }
        
        let invoice = try JSONDecoder().decode(Invoice.self, from: data)
        return invoice
    }
    
    func checkInvoiceStatus(invoiceId: String) async throws -> InvoiceStatus {
        guard let url = URL(string: "\(baseURL)/invoices/\(invoiceId)") else {
            throw PaymentError.invalidURL
        }
        
        var request = URLRequest(url: url)
        request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization")
        
        let (data, _) = try await URLSession.shared.data(for: request)
        let status = try JSONDecoder().decode(InvoiceStatus.self, from: data)
        return status
    }
}

struct Invoice: Codable {
    let id: String
    let amount: String
    let currency: String
    let depositAddress: String
    let qrCode: String?
    let status: String
    let expiresAt: String
}

struct InvoiceStatus: Codable {
    let status: String
    let confirmations: Int?
}

Step 2: Create Payment View

import SwiftUI

struct PaymentView: View {
    @StateObject private var paymentService = PaymentService(apiKey: "YOUR_API_KEY")
    @State private var invoice: Invoice?
    @State private var paymentStatus: String = "pending"
    @State private var isLoading = false
    
    let amount: String
    let description: String
    
    var body: some View {
        VStack(spacing: 20) {
            if let invoice = invoice {
                // QR Code
                if let qrCodeData = invoice.qrCode,
                   let qrImage = UIImage(data: Data(base64Encoded: qrCodeData) ?? Data()) {
                    Image(uiImage: qrImage)
                        .resizable()
                        .scaledToFit()
                        .frame(width: 300, height: 300)
                }
                
                // Payment Details
                VStack(alignment: .leading, spacing: 10) {
                    Text("Amount: \(invoice.amount) \(invoice.currency)")
                        .font(.headline)
                    
                    Text("Address: \(invoice.depositAddress)")
                        .font(.caption)
                        .textSelection(.enabled)
                    
                    Button("Copy Address") {
                        UIPasteboard.general.string = invoice.depositAddress
                    }
                    .buttonStyle(.bordered)
                }
                
                // Status
                Text("Status: \(paymentStatus)")
                    .foregroundColor(statusColor)
                
                // Deep Link to Wallet
                if let walletURL = createWalletDeepLink(address: invoice.depositAddress, amount: invoice.amount) {
                    Link("Open in Wallet", destination: walletURL)
                        .buttonStyle(.borderedProminent)
                }
            } else if isLoading {
                ProgressView("Creating invoice...")
            }
        }
        .padding()
        .task {
            await createInvoice()
            await pollPaymentStatus()
        }
    }
    
    private func createInvoice() async {
        isLoading = true
        do {
            invoice = try await paymentService.createInvoice(amount: amount, description: description)
            paymentStatus = invoice?.status ?? "pending"
        } catch {
            print("Error creating invoice: \(error)")
        }
        isLoading = false
    }
    
    private func pollPaymentStatus() async {
        guard let invoice = invoice else { return }
        
        while paymentStatus != "CONFIRMED" && paymentStatus != "EXPIRED" {
            try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
            
            do {
                let status = try await paymentService.checkInvoiceStatus(invoiceId: invoice.id)
                paymentStatus = status.status
                
                if status.status == "CONFIRMED" {
                    // Payment confirmed, navigate to success screen
                    break
                }
            } catch {
                print("Error checking status: \(error)")
            }
        }
    }
    
    private var statusColor: Color {
        switch paymentStatus {
        case "CONFIRMED": return .green
        case "EXPIRED": return .red
        default: return .orange
        }
    }
    
    private func createWalletDeepLink(address: String, amount: String) -> URL? {
        // Trust Wallet
        if let url = URL(string: "trust://wc?uri=ethereum:\(address)?value=\(amount)") {
            if UIApplication.shared.canOpenURL(url) {
                return url
            }
        }
        
        // MetaMask Mobile
        if let url = URL(string: "metamask://wc?uri=ethereum:\(address)?value=\(amount)") {
            if UIApplication.shared.canOpenURL(url) {
                return url
            }
        }
        
        return nil
    }
}

Android Integration (Kotlin)

Step 1: Create Payment Service

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*

interface PaymentApi {
    @POST("invoices")
    suspend fun createInvoice(
        @Header("Authorization") auth: String,
        @Body request: InvoiceRequest
    ): Invoice
    
    @GET("invoices/{id}")
    suspend fun getInvoice(
        @Header("Authorization") auth: String,
        @Path("id") invoiceId: String
    ): Invoice
}

data class InvoiceRequest(
    val amount: String,
    val currency: String = "USDT",
    val description: String
)

data class Invoice(
    val id: String,
    val amount: String,
    val currency: String,
    val depositAddress: String,
    val qrCode: String?,
    val status: String,
    val expiresAt: String
)

class PaymentService(private val apiKey: String) {
    private val api: PaymentApi
    
    init {
        val retrofit = Retrofit.Builder()
            .baseUrl("https://api.fromchain.plus/v1/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        
        api = retrofit.create(PaymentApi::class.java)
    }
    
    suspend fun createInvoice(amount: String, description: String): Invoice {
        return api.createInvoice(
            auth = "Bearer $apiKey",
            request = InvoiceRequest(amount, "USDT", description)
        )
    }
    
    suspend fun checkInvoiceStatus(invoiceId: String): Invoice {
        return api.getInvoice("Bearer $apiKey", invoiceId)
    }
}

Step 2: Create Payment Activity

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class PaymentActivity : ComponentActivity() {
    private val paymentService = PaymentService("YOUR_API_KEY")
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val amount = intent.getStringExtra("amount") ?: "0"
        val description = intent.getStringExtra("description") ?: ""
        
        setContent {
            PaymentScreen(amount, description)
        }
    }
    
    @Composable
    fun PaymentScreen(amount: String, description: String) {
        var invoice by remember { mutableStateOf<Invoice?>(null) }
        var status by remember { mutableStateOf("pending") }
        var isLoading by remember { mutableStateOf(false) }
        val scope = rememberCoroutineScope()
        
        LaunchedEffect(Unit) {
            isLoading = true
            invoice = paymentService.createInvoice(amount, description)
            isLoading = false
            
            // Poll for payment status
            scope.launch {
                while (status != "CONFIRMED" && status != "EXPIRED") {
                    delay(5000)
                    invoice?.let {
                        val updated = paymentService.checkInvoiceStatus(it.id)
                        status = updated.status
                        invoice = updated
                    }
                }
            }
        }
        
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.spacedBy(16.dp)
        ) {
            when {
                isLoading -> {
                    CircularProgressIndicator()
                    Text("Creating invoice...")
                }
                invoice != null -> {
                    // QR Code
                    invoice?.qrCode?.let { qrCode ->
                        Image(
                            painter = rememberAsyncImagePainter(qrCode),
                            contentDescription = "Payment QR Code",
                            modifier = Modifier.size(300.dp)
                        )
                    }
                    
                    // Payment Details
                    Text("Amount: ${invoice?.amount} ${invoice?.currency}", style = MaterialTheme.typography.headlineSmall)
                    Text("Address: ${invoice?.depositAddress}", style = MaterialTheme.typography.bodySmall)
                    
                    Button(onClick = {
                        // Copy to clipboard
                        val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
                        val clip = ClipData.newPlainText("Address", invoice?.depositAddress)
                        clipboard.setPrimaryClip(clip)
                    }) {
                        Text("Copy Address")
                    }
                    
                    // Status
                    Text(
                        "Status: $status",
                        color = when (status) {
                            "CONFIRMED" -> Color.Green
                            "EXPIRED" -> Color.Red
                            else -> Color.Orange
                        }
                    )
                    
                    // Open in Wallet
                    Button(onClick = {
                        openWalletApp(invoice?.depositAddress, invoice?.amount)
                    }) {
                        Text("Open in Wallet")
                    }
                }
            }
        }
    }
    
    private fun openWalletApp(address: String?, amount: String?) {
        address ?: return
        
        // Try Trust Wallet
        val trustIntent = Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse("trust://wc?uri=ethereum:$address?value=$amount")
        }
        if (trustIntent.resolveActivity(packageManager) != null) {
            startActivity(trustIntent)
            return
        }
        
        // Try MetaMask
        val metaMaskIntent = Intent(Intent.ACTION_VIEW).apply {
            data = Uri.parse("metamask://wc?uri=ethereum:$address?value=$amount")
        }
        if (metaMaskIntent.resolveActivity(packageManager) != null) {
            startActivity(metaMaskIntent)
        }
    }
}

Best Practices

1. Secure API Key Storage

Never store API keys in mobile apps!

  • Store keys on your backend
  • Mobile app calls your backend
  • Backend calls payment gateway

2. QR Code Display

  • Use high contrast colors
  • Minimum 300x300 pixels
  • Include error correction
  • Test on various screen sizes

3. Deep Linking to Wallets

Support multiple wallet apps:

func openWalletApp(address: String, amount: String) {
    let wallets = [
        "trust://wc?uri=ethereum:\(address)?value=\(amount)",
        "metamask://wc?uri=ethereum:\(address)?value=\(amount)",
        "coinbase-wallet://wc?uri=ethereum:\(address)?value=\(amount)"
    ]
    
    for walletURL in wallets {
        if let url = URL(string: walletURL),
           UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
            return
        }
    }
    
    // Fallback: Show address for manual copy
    showAddressForCopy(address)
}

4. Payment Status Polling

  • Poll every 5-10 seconds
  • Stop polling after confirmation or expiration
  • Show clear status messages
  • Handle network errors gracefully

5. Offline Support

func checkPaymentStatus() async {
    do {
        let status = try await paymentService.checkInvoiceStatus(invoiceId: invoice.id)
        updateUI(status)
    } catch {
        if error.isNetworkError {
            // Queue for retry when online
            queueStatusCheck()
        } else {
            showError(error)
        }
    }
}

Testing

iOS Testing

func testInvoiceCreation() async throws {
    let service = PaymentService(apiKey: "test_key")
    let invoice = try await service.createInvoice(amount: "10.00", description: "Test")
    
    XCTAssertNotNil(invoice.id)
    XCTAssertEqual(invoice.amount, "10.00")
    XCTAssertEqual(invoice.currency, "USDT")
}

Android Testing

@Test
fun testInvoiceCreation() = runTest {
    val service = PaymentService("test_key")
    val invoice = service.createInvoice("10.00", "Test")
    
    assertNotNull(invoice.id)
    assertEquals("10.00", invoice.amount)
    assertEquals("USDT", invoice.currency)
}

Common Issues and Solutions

Issue 1: QR Code Not Scanning

Solution: Increase QR code size, improve contrast, test on multiple devices

Issue 2: Deep Links Not Working

Solution: Check URL scheme, test on real devices, provide fallback options

Issue 3: Payment Status Not Updating

Solution: Implement proper polling with exponential backoff, handle network errors

Issue 4: App Store Rejection

Solution: For digital goods, crypto payments are allowed. For physical goods, may need approval.

Conclusion

Integrating crypto payments into mobile apps opens new opportunities for global reach and reduced costs. With proper implementation, you can provide a seamless payment experience for your users.

Get started with mobile crypto payments and expand your app's payment options today!