Реализация системы виртуальной валюты (баллы/монеты) в мобильном приложении
Виртуальная валюта — это не просто счётчик в базе данных. Это экономическая система внутри приложения, у которой есть инфляция, читерство и требования к транзакционности. Баги в начислении монет — это либо пустые кошельки пользователей (злые отзывы), либо переполнение (убытки для бизнеса).
Транзакционность — основа
Каждая операция с балансом — это транзакция с debit/credit записью в ledger таблице, не просто UPDATE balance = balance + N. Причина: атомарность. Если начислить 100 монет и одновременно списать 50 за покупку — при гонке двух запросов к полю баланса получаем некорректный результат. Ledger + SELECT FOR UPDATE или оптимистичная блокировка (UPDATE balance WHERE balance = :expected) на сервере.
Клиент при любой транзакции получает полный ответ: {balance: 1250, delta: +100, transaction_id: "uuid", reason: "daily_bonus"}. Никогда не пересчитываем баланс на клиенте — только отображаем значение с сервера.
Защита от читерства
Отладочный прокси. Charles Proxy или mitmproxy позволяет пересылать запросы и изменять ответы. Если ответ /rewards/daily-bonus возвращает {"coins": 100} без подписи сервера — клиент может показать «получил 100 монет», но сервер в это время должен самостоятельно начислять и не доверять значению из ответа. Баланс на клиенте — только для отображения.
SSL Pinning. Базовая защита от MITM: TrustKit (iOS) / OkHttp CertificatePinner (Android). Не абсолютная защита — Frida или root + SSL Kill Switch обходят pinning. Но отсекает 90% script-kiddie попыток.
Rate limiting операций. Двойное нажатие кнопки «Собрать бонус» — debounce на клиенте (250ms throttle) плюс серверная защита через idempotency_key (UUID от клиента). Сервер игнорирует повторный запрос с тем же ключом в течение 60 секунд.
UI: анимация начисления монет
Монеты «летят» к счётчику — стандартная анимация для reward. Реализация: частицы через CAEmitterLayer (iOS) или Jetpack Compose Canvas с кастомным анимированным Modifier. Несколько монет-иконок вылетают из источника (позиция кнопки/иконки события), летят по кривой Безье к виджету баланса, при каждом «попадании» счётчик инкрементируется на 1. Плавный нарастающий эффект через CAKeyframeAnimation с path.
Счётчик баланса при обновлении — number rolling animation: цифры прокручиваются сверху вниз как одометр. На iOS через UILabel с CATransition или кастомный AnimatedNumberView в SwiftUI. На Android — ValueAnimator с TextSwitcher.
Покупка монет через IAP
Consumable продукты в StoreKit 2: «100 монет за $0.99», «550 монет за $4.99» и т.д. Product.purchase() → Transaction.finish() после начисления серверного баланса — важный порядок: сначала зачисляем монеты через сервер (серверная валидация чека), потом завершаем транзакцию. Если завершить транзакцию до зачисления и приложение упадёт — пользователь заплатил, монеты не получил.
На Android: BillingClient.launchBillingFlow() → Purchase.getPurchaseState() == PURCHASED → серверная валидация через Google Play Developer API → BillingClient.consumeAsync() (consumable должен быть consumed, иначе недоступен для повторной покупки).
Pending transactions (отложенные покупки на Android через семейный Google Pay или банковское подтверждение) — обрабатываем через BillingClient.PurchasesUpdatedListener, ждём подтверждения до начисления монет.
История транзакций
Пользователь хочет понять, куда ушли монеты. TransactionHistory — пагинированный список (cursor-pagination) с типами: earned_daily_bonus, earned_referral, spent_purchase, spent_unlock, purchased_iap. Фильтр по типу. Локальный кеш последних 50 операций в Core Data / Room для мгновенного отображения без сетевого запроса.
Процесс работы
Проектирование схемы ledger и защиты от читерства → разработка IAP (consumable) + серверная валидация → UI баланса + анимации → история транзакций → QA (включая тест parallel requests, pending transactions) → публикация.
Ориентиры по срокам
Полная реализация системы виртуальной валюты с IAP, анимациями и историей транзакций — 3–5 рабочих дней при готовом серверном API. Если включает проектирование серверного ledger — 1–2 недели.







