Stripe Payment Gateway Integration in Mobile Application
Stripe is the technically most mature payment gateway from a mobile SDK perspective. Stripe iOS and Android SDKs cover not only basic card input, but also Apple Pay, Google Pay, 3DS2, Link, and payment method storage via SetupIntent. Selecting the correct flow depends on the task: one-time payment, subscription, or card storage without immediate charge — these are different APIs.
PaymentIntent vs SetupIntent: Which to Use
PaymentIntent — for immediate charge. Created on server, passed to client via client_secret, client confirms.
SetupIntent — for card storage without charge (for example, during registration to charge later via API). Similar flow, but without amount.
The main mistake is creating PaymentIntent on the client. secret_key must never reach the application. Only publishable_key — client-side.
iOS: PaymentSheet and Custom Flow
Stripe offers two approaches: ready-made PaymentSheet (native UI from Stripe) and element-by-element STPPaymentHandler.
PaymentSheet (Recommended for Start)
import StripePaymentSheet
var paymentSheet: PaymentSheet?
func preparePaymentSheet(clientSecret: String, customerId: String, ephemeralKeySecret: String) {
var config = PaymentSheet.Configuration()
config.merchantDisplayName = "Your Company"
config.customer = .init(id: customerId, ephemeralKeySecret: ephemeralKeySecret)
config.applePay = .init(
merchantId: "merchant.com.yourcompany.app",
merchantCountryCode: "US"
)
config.defaultBillingDetails.address.country = "RU"
config.allowsDelayedPaymentMethods = true
paymentSheet = PaymentSheet(
paymentIntentClientSecret: clientSecret,
configuration: config
)
}
@IBAction func checkoutTapped(_ sender: UIButton) {
paymentSheet?.present(from: self) { [weak self] result in
switch result {
case .completed:
self?.handleSuccess()
case .failed(let error):
print("Payment failed: \(error.localizedDescription)")
case .canceled:
break
}
}
}
Custom Flow with CardField
If full UI control is needed:
let cardField = STPPaymentCardTextField()
// Confirm payment
STPPaymentHandler.shared().confirmPayment(
paymentParams,
with: self
) { [weak self] status, paymentIntent, error in
switch status {
case .succeeded:
self?.handleSuccess()
case .failed:
print("Error: \(error?.localizedDescription ?? "")")
case .canceled:
break
@unknown default:
break
}
}
Android: PaymentSheet and CardInputWidget
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetResult
private lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paymentSheet = PaymentSheet(this) { result ->
when (result) {
is PaymentSheetResult.Completed -> handleSuccess()
is PaymentSheetResult.Failed -> {
Log.e("Stripe", result.error.message ?: "Unknown error")
}
is PaymentSheetResult.Canceled -> {}
}
}
}
fun launchPaymentSheet(clientSecret: String, customerId: String, ephemeralKey: String) {
val config = PaymentSheet.Configuration(
merchantDisplayName = "Your Company",
customer = PaymentSheet.CustomerConfiguration(customerId, ephemeralKey),
googlePay = PaymentSheet.GooglePayConfiguration(
environment = PaymentSheet.GooglePayConfiguration.Environment.Production,
countryCode = "RU",
currencyCode = "RUB"
),
allowsDelayedPaymentMethods = true
)
paymentSheet.presentWithPaymentIntent(clientSecret, config)
}
3DS2: What Happens Under the Hood
Stripe SDK handles 3DS2 automatically within confirmPayment / PaymentSheet.present. When the bank requires verification, SDK opens native 3DS2 challenge (biometry or OTP) directly in the application — without browser redirect. This is important: 3DS1 redirect via WebView often loses the callback, and the transaction hangs.
If your provider returns requires_action in PaymentIntent status — this is normal, Stripe SDK will handle the challenge.
Typical Issues
No such PaymentIntent — client uses client_secret from different environment (test key with live secret or vice versa). Publishable key and client_secret must be from the same environment.
PaymentSheet not opening on Android, no error. Stripe PaymentSheet requires FragmentActivity, not plain Activity. Running from regular Activity — you get silent refusal.
Ephemeral key expired. Stripe Ephemeral Keys live 1 hour. If user sits on payment screen for long — key expires, PaymentSheet crashes trying to load saved payment methods. Must refresh key before opening sheet.
Server Side (Minimal Backend)
# FastAPI / Django / Laravel — logic is identical
stripe.api_key = settings.STRIPE_SECRET_KEY
@app.post("/create-payment-intent")
async def create_payment_intent(amount: int, currency: str = "rub"):
intent = stripe.PaymentIntent.create(
amount=amount, # in kopecks
currency=currency,
automatic_payment_methods={"enabled": True},
)
return {"clientSecret": intent.client_secret}
Work Scope
- PaymentSheet or custom card flow implementation on iOS and Android
- Server endpoint for PaymentIntent / SetupIntent creation
- Apple Pay and Google Pay integration via Stripe
- Webhook configuration for final payment status confirmation
- Testing with Stripe test cards (4242 4242 4242 4242 and 3DS scenarios)
Timeline
3–5 days for full integration with Apple Pay, Google Pay, and 3DS2. Basic card flow only — 1–2 days. Cost calculated individually.







