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!
