Реализация внутриигрового магазина мобильной игры
Внутриигровой магазин — центральная точка монетизации игры. Игрок открывает его с намерением потратить. Задача реализации: не мешать этому намерению техническими проблемами и сделать процесс покупки максимально простым.
Архитектура каталога
Каталог магазина хранится на сервере — никаких захардкоженных цен и товаров на клиенте. Это позволяет менять предложения без апдейта приложения, проводить A/B-тесты и запускать акции в реальном времени.
Структура товара:
{
"productId": "gems_pack_medium",
"type": "iap_consumable",
"storeProductId": {
"ios": "com.mygame.gems.500",
"android": "gems_500"
},
"displayName": "500 кристаллов",
"description": "Плюс 50 бонусных кристаллов",
"gemAmount": 500,
"bonusGemAmount": 50,
"badge": "best_value",
"position": 2,
"isVisible": true
}
storeProductId — разные для iOS и Android, так как product ID в App Store и Google Play независимы. Клиент выбирает нужный по платформе при отображении.
Покупка IAP: технический поток
// iOS - StoreKit 2
func purchaseProduct(_ product: Product) async throws -> PurchaseResult {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
// Верификация на сервере
let serverVerified = await verifyWithServer(transaction)
if serverVerified {
await transaction.finish()
return .success
} else {
// Не финишируем транзакцию — не выдаём товар
return .verificationFailed
}
case .unverified:
return .verificationFailed
}
case .userCancelled: return .cancelled
case .pending: return .pending
}
}
Не вызывать transaction.finish() до выдачи товара. Если финишировать транзакцию до подтверждения доставки — при сбое сервера товар не выдан, транзакция завершена, восстановить нельзя. Только после serverVerified = true.
Виртуальная валюта
Кошелёк виртуальной валюты хранится на сервере. Клиент отображает баланс, который получил с сервера при последней синхронизации, и обновляет после каждой транзакции.
Логика пополнения:
POST /shop/purchase
{ "productId": "gems_pack_medium", "receiptData": "...", "userId": "..." }
→ Сервер верифицирует receipt у Apple/Google
→ Проверяет, что транзакция не была обработана ранее (idempotency by transactionId)
→ Зачисляет 550 gems на баланс игрока
→ Возвращает { "newBalance": 1050, "transactionId": "..." }
Idempotency обязательна: если клиент отправил запрос дважды (сбой сети → retry), товар должен выдаться один раз, а не дважды.
История покупок
Экран истории покупок — частое требование и хорошая практика для снижения чарджбеков. Игрок видит все транзакции с датой, суммой и выданным товаром. Это снижает «я не помню что покупал» как причину dispute.
Технически: таблица purchase_history на сервере, пагинированный API, клиентский список с pull-to-refresh.
Ротируемые предложения и акции
Daily Deals, Flash Sales, персонализированные офферы — отдельный тип позиций магазина с validUntil timestamp. Клиент показывает таймер обратного отсчёта.
Для персонализации: Firebase Remote Config или собственный recommendation engine выбирает офферы на основе поведения игрока (что покупал, до какого уровня дошёл, как давно не открывал магазин).
Restore Purchases
На iOS обязательна кнопка «Восстановить покупки» для non-consumable IAP и подписок. Реализация через Transaction.currentEntitlements в StoreKit 2. На Android — автоматически при входе в Google Play, но кнопка в настройках всё равно хороша как UX.
Сроки: базовый магазин с несколькими товарами, IAP-интеграцией и серверной верификацией — 5 дней. Полная реализация с ротируемыми предложениями, историей, подписками и аналитикой — 2 недели.







