Интеграция платежного шлюза Stripe в мобильное приложение
Stripe — наиболее технически зрелый платёжный шлюз с точки зрения мобильного SDK. Stripe iOS и Android SDK покрывают не только базовый card input, но и Apple Pay, Google Pay, 3DS2, Link, а также сохранение методов оплаты через SetupIntent. Выбор правильного flow зависит от задачи: разовый платёж, подписка, сохранение карты без немедленного списания — это разные API.
PaymentIntent vs SetupIntent: что использовать
PaymentIntent — для немедленного списания. Создаётся на сервере, передаётся клиенту через client_secret, клиент подтверждает.
SetupIntent — для сохранения карты без списания (например, при регистрации, чтобы потом списывать через API). Аналогичный flow, но без суммы.
Главная ошибка — создавать PaymentIntent на клиенте. secret_key никогда не должен попадать в приложение. Только publishable_key — клиентский.
iOS: PaymentSheet и кастомный flow
Stripe предлагает два подхода: готовый PaymentSheet (нативный UI от Stripe) и поэлементный STPPaymentHandler.
PaymentSheet (рекомендуется для старта)
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
}
}
}
Кастомный flow с CardField
Если нужен полный контроль над UI:
let cardField = STPPaymentCardTextField()
// Подтверждение платежа
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 и 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: что происходит под капотом
Stripe SDK обрабатывает 3DS2 автоматически в рамках confirmPayment / PaymentSheet.present. Когда банк требует подтверждения, SDK открывает нативный 3DS2 challenge (биометрия или OTP) прямо в приложении — без редиректа в браузер. Это важно: 3DS1 redirect через WebView часто теряет callback, и транзакция зависает.
Если ваш провайдер возвращает requires_action в статусе PaymentIntent — это нормально, Stripe SDK сам обработает challenge.
Типичные проблемы
No such PaymentIntent — клиент использует client_secret от другого окружения (test key с live secret или наоборот). Publishable key и client_secret должны быть из одного окружения.
PaymentSheet не открывается на Android, нет ошибки. Stripe PaymentSheet требует FragmentActivity, не просто Activity. Если запускаете из обычной Activity — получаете тихий отказ.
Ephemeral key expired. Stripe Ephemeral Keys живут 1 час. Если пользователь долго сидит на экране оплаты — ключ протухает, PaymentSheet падает при попытке загрузить сохранённые методы оплаты. Нужно обновлять ключ перед открытием шита.
Серверная часть (минимальный бэкенд)
# FastAPI / Django / Laravel — логика одинакова
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, # в копейках
currency=currency,
automatic_payment_methods={"enabled": True},
)
return {"clientSecret": intent.client_secret}
Что входит в работу
- Реализация PaymentSheet или кастомного card flow на iOS и Android
- Серверный endpoint для создания PaymentIntent / SetupIntent
- Интеграция Apple Pay и Google Pay через Stripe
- Настройка Webhooks для финального подтверждения статуса платежа
- Тестирование с тестовыми картами Stripe (4242 4242 4242 4242 и сценарии 3DS)
Сроки
3–5 дней для полной интеграции с Apple Pay, Google Pay и 3DS2. Только базовый card flow — 1–2 дня. Стоимость рассчитывается индивидуально.







