Розробка мобільного додатку для продажу квитків на заходи
Головний технічний біль ticketing-додатку — конкурентний продаж. Коли 500 людей одночасно натискають «Купити» на останні 10 місць, додаток зобов’язаний коректно обробити це без подвійних продажів, зависань і некоректних статусів. Це не frontend-задача — ми проєктуємо серверну частину з песимістичними блокуваннями та резервуванням через чергу. А мобільний клієнт готовий до сценарію «місце вже зайняте» — зі зрозумілим UX та миттєвим зворотним зв’язком.
Ми створюємо ticketing-додатки під ключ: від інтерактивної схеми залу до додатку контролера. Наш досвід у цій сфері — 5+ років та понад 20 успішних проєктів. Гарантуємо відмовостійкість під навантаженням та дотримання App Store Review Guidelines (Section 4.2/5.1).
Резервування місць та race condition
Стандартна схема: користувач обирає місце → сервер створює reservation з TTL 10 хвилин → користувач платить → reservation конвертується в ticket. Якщо оплата не пройшла за 10 хвилин — місце звільняється.
На мобільному клієнті таймер резервування — це CountDownTimer (Android) або Timer.scheduledTimer (iOS), синхронізований із серверним TTL. Не з моменту натискання кнопки на клієнті, а з reservation.expiresAt з відповіді сервера. Різниця часових поясів і дрейф системного годинника пристрою вб’ють UI-таймер, якщо синхронізувати неправильно.
// Android: синхронізація таймера з серверним expiresAt
class ReservationViewModel : ViewModel() {
private val _timeLeft = MutableStateFlow(0L)
val timeLeft: StateFlow<Long> = _timeLeft
fun startCountdown(expiresAt: Instant) {
viewModelScope.launch {
while (true) {
val remaining = ChronoUnit.SECONDS.between(Instant.now(), expiresAt)
if (remaining <= 0) {
_timeLeft.emit(0)
onReservationExpired()
break
}
_timeLeft.emit(remaining)
delay(1000)
}
}
}
}
При закінченні резервування — не просто показуємо помилку. Автоматично пропонуємо найближчі доступні місця, якщо потік реального часу (WebSocket або SSE) це підтримує.
Як уникнути double booking?
Double booking — класична проблема при паралельних запитах. Ми вирішуємо її за допомогою SELECT FOR UPDATE в PostgreSQL або distributed lock на рівні reservationId + seatId в Redis. Альтернатива — ON CONFLICT DO NOTHING при вставці резервування. На клієнті дублюємо блокування: кнопка «Купити» деактивується після першого натискання, а повторні запити надсилаються з унікальним idempotency key. Ці методи в 3 рази знижують ймовірність конфлікту при пікових навантаженнях.
Схема місць та інтерактивний зал
Для концертних залів і театрів потрібна інтерактивна схема. На iOS — UICollectionView з кастомним UICollectionViewLayout або Canvas в SwiftUI для залів з нестандартною геометрією. На Android — Canvas + GestureDetector для pinch-to-zoom.
Схема завантажується як JSON з координатами кожного сидіння, секцією та статусом (available, reserved, sold, disabled). Оновлення статусів у реальному часі — через WebSocket: як тільки хтось зарезервував місце, всім підключеним клієнтам летить { type: "seat_reserved", seatId: "A-15" }.
| Платформа |
Технологія |
Перевага |
| iOS |
SwiftUI Canvas + UIKit |
Нативна продуктивність, підтримка безьє |
| Android |
Canvas + GestureDetector |
Плавний pinch-to-zoom, кастомна геометрія |
| Cross-platform |
react-native-svg / flutter_svg |
Єдиний код, швидкий рендеринг SVG |
| Технологія оновлення |
Затримка |
Навантаження на сервер |
| WebSocket |
~100 мс |
Низьке (постійне з’єднання) |
| Polling (кожні 5 с) |
~5 с |
Високе (часті запити) |
SVG-схеми залів — популярний варіант для крос-платформних рішень. React Native з react-native-svg або Flutter з flutter_svg + GestureDetector рендерять SVG з можливістю дотику по елементах.
Чому важливо оновлення в реальному часі?
Без WebSocket користувач може обрати місце, яке вже зайняте іншим. Це призводить до фрустрації та втрати продажів. Ми впроваджуємо WebSocket-з’єднання з автоматичним перепідключенням (exponential backoff). Як тільки резервування змінюється, клієнт отримує подію та перемальовує схему. Для offline-режиму кешуємо останній стан і показуємо попередження.
Електронні квитки та валідація на вході
Кожен куплений квиток отримує унікальний UUID та QR-код, згенерований на сервері. Зберігати секрет у QR не потрібно — достатньо ticketId, верифікація відбувається на сервері при скануванні. QR не має бути статичним зображенням у PDF — генеруємо його динамічно з ticketId на клієнті через ZXingObjC (iOS) або zxing-android-embedded, щоб не зберігати растрове зображення в базі.
Додаток контролера — окремий екран або окремий додаток зі сканером AVFoundation/CameraX, POST на /tickets/{id}/validate, відповідь: valid / already_used / invalid. Кешуємо валідовані ticketId локально на пристрої контролера на випадок відсутності інтернету — синхронізація після відновлення зв’язку.
Платіжний флоу
Еквайринг — ЮKassa, CloudPayments або Stripe. На iOS додатково Apple Pay (PKPaymentRequest), на Android — Google Pay. Для b2b сегменту — виставлення рахунку на email з оплатою через СБП.
Після оплати негайна відправка PDF-квитка на email через SendGrid/Postmark і push-сповіщення через FCM/APNs з deep link на екран «Мої квитки».
Як ми це робимо: стек і процес
...
Приклад конфігурації кешування контролера
CREATE TABLE ticket_validations (
ticket_id UUID PRIMARY KEY,
validated_at TIMESTAMP DEFAULT NOW()
);
При відновленні зв’язку надсилаємо всі ticket_id на сервер для синхронізації.
Типові помилки
- Double booking. Без
SELECT FOR UPDATE або distributed lock два користувачі можуть створити резервування на одне місце.
- QR на скріншоті. Не ставте захист від скріншотів. QR має бути одноразовим при валідації.
- Синхронізація часу. Використовуйте серверне
expiresAt, а не локальний час.
Етапи та терміни
Проектування моделі даних → схема місць → резервування з TTL → оплата → електронні квитки → додаток контролера → тестування навантаження (1000+ одночасних користувачів).
6–10 тижнів для повноцінного додатку з інтерактивною схемою залу та real-time оновленнями. Вартість розраховується індивідуально після аналізу вимог.
Отримайте консультацію з архітектури ticketing-рішення. Замовте розробку під ваш проєкт — оцінку зробимо за 1 день.
Платежі в мобільних додатках: In-App Purchase, StoreKit 2, Google Billing, Stripe, RevenueCat
У кожному нашому проєкті з монетизації додатку ми балансуємо між політиками App Store та Google Play, вимогами PCI DSS та логікою верифікації покупок на бекенді. Неправильно зроблена система платежів — це не просто баг, це фінансові втрати та можливий бан додатку. За 7 років ми розібрали понад 50 кейсів інтеграції платіжних SDK — від простої Stripe-форми до розподіленого біллінгу з власними серверними вебхуками.
Як вибрати між In-App Purchase та зовнішнім платіжним шлюзом?
Якщо додаток продає цифровий контент або підписки — Apple та Google вимагають використовувати їхні платіжні системи. Обійти це неможливо: порушення правил 3.1.1 App Store або Google Play Developer Policy призводить до видалення додатку. Фізичні товари та послуги, що надаються офлайн, — інша історія.
In-App Purchase: дві платформи, два різних API
StoreKit 2 (iOS 15+)
StoreKit 2 — повна переробка оригінального StoreKit з async/await API. Product.products(for:), product.purchase(), Transaction.currentEntitlements — читабельно та передбачувано порівняно з чергою транзакцій через SKPaymentTransactionObserver.
Найважливіша зміна: транзакції в StoreKit 2 підписані JWS (JSON Web Signature) і верифікуються локально без серверного roundtrip. Transaction.verificationResult повертає .verified(Transaction) або .unverified(Transaction, VerificationError). Це не означає, що сервер не потрібен — він потрібен для зберігання статусу підписки, але локальна верифікація прибирає затримку при старті.
StoreKit.AppTransaction — верифікація самого факту завантаження додатку з App Store. Потрібна для додатків з платним завантаженням або безстроковими покупками, не підписками.
Складне місце в StoreKit 2 — обробка renewalState для підписок: .subscribed, .expired, .inBillingRetryPeriod, .inGracePeriod, .revoked. Стан inGracePeriod означає, що Apple намагається відновити оплату (до 16 днів) — у цей час потрібно продовжувати надавати доступ. Не обробиш — втратиш лояльних користувачів, у яких тимчасово не пройшла картка. За досвідом, близько 5% підписок потрапляють у billing retry, і автоматичне відновлення доступу повертає до 80% з них.
Google Play Billing Library (v6+)
Google Billing — складніший за StoreKit за кількістю сценаріїв. BillingClient з PurchasesUpdatedListener, queryProductDetailsAsync, launchBillingFlow, queryPurchasesAsync — обов'язково викликати при кожному старті додатку, не покладатися на PurchasesUpdatedListener як єдине джерело правди.
Підтвердження покупки: acknowledgePurchase() для non-consumables та підписок, consumePurchase() для consumables. Якщо не викликати acknowledge протягом 3 днів — Google автоматично зробить повернення. Це гарантована втрата грошей, якщо забути про acknowledge на бекенді після верифікації.
ProductDetails з SubscriptionOfferDetails — в Billing v5+ структура оферт ускладнилася: один продукт може мати кілька basePlanId та offerId (пробний період, знижка для нових користувачів, retention-офер). BillingFlowParams.SubscriptionUpdateParams для апгрейду/даунгрейду підписки з prorationMode.
Чому серверна верифікація обов'язкова?
Ніколи не довіряйте лише клієнтському коду при розблокуванні платного контенту. Клієнтська верифікація обходиться модифікацією додатку.
Для IAP мінімальна схема: додаток отримує receiptData (iOS) або purchaseToken (Android), відправляє на бекенд, бекенд верифікує через Apple App Store Server API / Google Play Developer API, зберігає статус у БД, віддає відповідь клієнту. RevenueCat робить це за вас — але якщо у вас кастомний бекенд, потрібно реалізувати самостійно.
Webhook-и важливіші, ніж здається. Користувач може скасувати підписку через налаштування телефону, не через додаток — додаток не отримає про цю подію в реальному часі. Тільки webhook від Apple/Google (або RevenueCat) дозволяє своєчасно оновити статус. Ми використовуємо перевірку підпису вхідних запитів через Apple's signedPayload та Google's DeveloperNotification.
Як RevenueCat спрощує інтеграцію?
Підтримувати StoreKit 2 та Google Billing одночасно, з урахуванням промо-кодів, оферт, відновлення покупок та серверної верифікації — це кілька місяців розробки. RevenueCat закриває більшу частину цього шару.
RevenueCat — не просто SDK для платежів. Це:
- Єдиний API для iOS та Android (і Stripe для вебу)
- Серверна верифікація та зберігання статусів підписок
- Webhooks на події (покупка, відновлення, скасування, billing issue)
- Аналітика по когортам, MRR, churn
- A/B тестування оферт через Experiments
Purchases.configure(withAPIKey:) при старті, Purchases.shared.getCustomerInfo() для отримання поточних entitlements — мінімальний інтегрований шар. Purchases.shared.purchase(package:) замість прямого виклику StoreKit/Billing.
У документації RevenueCat сказано: «RevenueCat handles receipt validation on the server side, reducing client-side complexity and preventing fraudulent purchases.»
Обмеження RevenueCat: платний (безкоштовно до певного рівня MRR, далі відсоток від доходу), не підходить для дуже складних flow з кількома storefront-ами або кастомними bundle-ами. Однак для типового SaaS-додатку економія на власній розробці є значною — інтеграція окупається швидко.
Stripe в мобільних додатках
Stripe — для оплати фізичних товарів, послуг, B2B-платежів де IAP не вимагається політикою платформи.
Stripe iOS SDK та Android SDK — PaymentSheet для готового UI оплати, PaymentSheetFlowController для кастомного UI зі збереженими картками. Payment Intent створюється на сервері, client secret передається в додаток — карткові дані ніколи не проходять через ваш сервер, лише через Stripe.
Apple Pay та Google Pay через Stripe: PKPaymentRequest (iOS) та GooglePayLauncher (Android) вже інтегровані в Stripe SDK. Конверсія у Apple Pay дає в 1.3–2 рази вищий результат, ніж форми з ручним введенням картки — це цифри, які ми підтвердили на десятці проєктів.
Збережені картки через SetupIntent + Customer API — користувач платить в один тап при повторному візиті. Compliance: PCI DSS SAQ A — найлегший рівень compliance, оскільки Stripe Tokenization прибирає потребу зберігати карткові дані на своїй стороні. Згідно з PCI DSS, передача токенів звільняє від необхідності сертифікації рівня 1.
3DS2 (Strong Customer Authentication) — обов'язковий для платежів у ЄС за PSD2. Stripe обробляє автоматично через PaymentIntent.confirmPayment, але потрібно коректно обробити .requiresAction статус та повернути користувача на потрібний екран після аутентифікації.
Що входить в роботу (deliverables)
| Документація/Артефакт |
Зміст |
| Архітектурна схема біллінгу |
Діаграма потоків: клієнт → SDK → сервер → store/webhook |
| Інтеграція SDK |
Підключення та конфігурація StoreKit 2, Google Billing, RevenueCat або Stripe |
| Серверна верифікація |
Реалізація ендпоінтів та обробка webhook (Apple/Google/RevenueCat) |
| Тестовий стенд |
Sandbox Apple, License Testers Google, Stripe Test Mode |
| Документація по запуску |
Опис ключів, provisioning profiles, TestFlight |
| Навчання команди |
Сесія з підтримки платіжного модуля |
Процес та терміни
Починаємо з з'ясування бізнес-моделі: підписки, разові покупки, consumables, freemium. Від цього залежить архітектура. Тестування IAP вимагає Sandbox-акаунтів (Apple) та License Testers (Google) — це окреме налаштування середовища.
Sandbox Apple поводиться не так, як production: підписки відновлюються кожні 5 хвилин замість місяця, inGracePeriod працює по-іншому. Обов'язково тестувати сценарії: закінчення тріалу, скасування, billing retry, refund.
Як налаштувати тестове середовище для платежів?
| Сценарій |
Інструмент |
Час реалізації |
| Підписки iOS + Android |
StoreKit 2 + Google Billing + RevenueCat |
2–3 тижні |
| Підписки з кастомним бекендом |
StoreKit 2 + Google Billing + власний webhook |
4–6 тижнів |
| Оплата карткою (фізичні товари) |
Stripe PaymentSheet |
1–2 тижні |
| Apple Pay / Google Pay |
Stripe або нативні SDK |
+ 3–5 днів |
| Повний платіжний стек |
Все вищеперераховане |
6–10 тижнів |
Розгорнути типові помилки при інтеграції
- Забули викликати
acknowledgePurchase() на Android — гроші повертаються через 3 дні.
- Не обробили
inGracePeriod — лояльні користувачі блокуються без доступу.
- Поклалися лише на Pushtokens при відновленні підписок — пропускаєте state оновлення.
- Використовували production-ключі в TestFlight — спрацьовують реальні списання.
Вартість розраховується індивідуально виходячи з набору інструментів та складності серверної логіки. У середньому ми вкладаємося в бюджет, який обговорюється окремо, але економія від запобігання помилок та churn окупає ці вкладення за 2–3 місяці.
Отримайте консультацію по вашому проєкту — зв'яжіться з нами. Ми допоможемо обрати оптимальну архітектуру платежів, яка пройде рев'ю сторів та не зламається при пікових навантаженнях.