Bỏ qua để đến nội dung

Mua trong ứng dụng (IAP)

Gói GEM/GOLD, gỡ quảng cáo… bằng tiền thật. Portal là nơi quyết định cộng gì cho người chơi, sau khi tự xác minh với Google.

GameGoogle Play BillingPortal backendAndroid Publisher API launchBillingFlow(SKU, obfuscatedAccountId = player_uid) purchase_token POST …/client/players/{player_uid}/purchases/verify GET purchases/products/{SKU}/tokens/{token} purchaseState, obfuscatedExternalAccountId kiểm account + cộng thưởng (ledger) :consume (gói tiền tệ) / :acknowledge (khác) kết quả (lỗi ack/consume → cron 5 phút thử lại)
Xác thực với Android Publisher API bằng service account (scope androidpublisher). Cron reconcile_voided_purchases.py 02:25 hằng ngày quét giao dịch bị hoàn tiền/hủy 48 giờ gần nhất và thu hồi.

4.1 — Tạo sản phẩm (Play Console)

  1. Play Console → app → Monetize → Products → In-app products (cần đã upload một bản build có thư viện Billing; Drone Strike đã có qua SDK portal_iap). Bật payments profile nếu được yêu cầu.
  2. Create product: Product ID (ví dụ gem_pack_100 — không đổi được sau khi tạo), tên, mô tả, giá → Activate. Gói tiền tệ (mua nhiều lần) và sản phẩm mua một lần (gỡ quảng cáo) đều là in-app product loại managed; Portal tự consume gói tiền tệ (product_type = currency_pack), các loại khác được acknowledge.
  3. Subscription không được hỗ trợ (Portal từ chối thông báo subscription).

4.2 — Tạo service account (Google Cloud Console)

  1. Cùng project ở mục 03 → APIs & Services → Library → bật Google Play Android Developer API.
  2. IAM & Admin → Service Accounts → Create service account, tên ví dụ portal-play. Không cần cấp role IAM trong Cloud.
  3. Mở service account → Keys → Add key → Create new key → JSON. Tải file về (ví dụ play.json). File này là secret: không commit, không gửi qua chat công khai. Ghi lại email dạng portal-play@<project>.iam.gserviceaccount.com.

4.3 — Cấp quyền cho service account (Play Console)

  1. Play Console (cấp tài khoản developer, không phải trong app) → Users and permissions → Invite new users → dán email service account.
  2. Tab App permissions → thêm app Drone Strike, cấp tối thiểu: View app information, View financial data, Manage orders and subscriptions (để đọc giao dịch, acknowledge/consume và đọc voided purchases). Repo không quy định danh sách quyền; đây là bộ quyền Google yêu cầu cho các API đang gọi.
  3. Invite user. Quyền có thể mất tới vài giờ (đôi khi tới 24 giờ) mới có hiệu lực — trong thời gian đó Google trả 401/403.

4.4 — Cài vào VPS

  1. Thư mục credentials. docker-compose.prod.yml đã mount ./credentials (thư mục cài đặt) vào backend tại /app/credentials, chỉ đọc; deploy_local.sh tự tạo thư mục với quyền 700. Backend chạy bằng UID 1000 — trùng user deploy huynqn trên VPS, nên file 600 của user đó đọc được.
  2. Đặt file lên VPS (thư mục cài đặt production: /home/huynqn/web/web-portal):
    máy bạn → VPS
    $ ssh huynqn@20.51.243.159 'mkdir -p /home/huynqn/web/web-portal/credentials && chmod 700 /home/huynqn/web/web-portal/credentials'
    $ scp play.json huynqn@20.51.243.159:/home/huynqn/web/web-portal/credentials/play.json
    $ ssh huynqn@20.51.243.159 'chmod 600 /home/huynqn/web/web-portal/credentials/play.json'
    Nếu user deploy không có UID 1000, đặt chủ file là UID 1000 (sudo chown 1000 play.json) để backend đọc được.
  3. Thêm vào .env (sao lưu trước: cp -p .env .env.bak.$(date -u +%Y%m%d_%H%M%S)):
    .env trên VPS
    GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH=/app/credentials/play.json
    # Khóa Fernet riêng để mã hóa purchase token (khuyến nghị; bỏ trống thì dùng MFA_ENCRYPTION_KEY)
    IAP_TOKEN_ENCRYPTION_KEY=<khóa Fernet>
    Sinh khóa Fernet: python3 -c 'from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())' (hoặc chạy trong container backend). Giữ khóa này cố định — đổi khóa thì token đã lưu không giải mã được.
  4. Tạo lại backend: docker compose -f docker-compose.prod.yml up -d backend. File credential cũng được backup_config.sh sao lưu (mã hóa) mỗi đêm; job báo lỗi nếu .env trỏ tới file không tồn tại.

4.5 — Cấu hình trong Portal

  1. Shop: tạo sản phẩm giá REAL, trạng thái published, đang bán (listed), trường Google Play Product ID (SKU) = Product ID ở 4.1. Sản phẩm phải có icon thì cửa hàng trong game mới hiện.
  2. Feature Flags: bật iap (mặc định tắt). Khi tắt, API trả 403 iap_disabled và cửa hàng không hiện sản phẩm tiền thật.
!

Bật cờ iap trước khi có service account thì mọi giao dịch đều xác minh lỗi (người chơi bị trừ tiền ở Google nhưng chưa được cộng, phải chờ cron/hoàn tiền). Chỉ bật sau khi 4.2–4.4 xong và mua thử thành công.

4.6 — Mua thử

  1. Play Console (cấp tài khoản) → Settings → License testing: thêm email Google của người test — mua không mất tiền thật.
  2. Cài game từ track Internal testing (cài qua link opt-in, không sideload) bằng tài khoản đó, mua một gói.
  3. Kiểm Portal → Purchases: giao dịch trạng thái thành công; số dư người chơi tăng đúng một lần; mua gói tiền tệ lần nữa được (đã consume).
  4. Hoàn tiền thử ở Play Console → Order management: sau lần chạy cron 02:25 (giờ VPS) phần thưởng bị thu hồi; trường hợp thiếu số dư hiện ở Economy Anomalies.
Lỗi trả về gameNguyên nhânXử lý
403 iap_disabledCờ iap của game đang tắtFeature Flags → bật iap
404 unknown_productSKU không khớp sản phẩm REAL đã publish và đang bán trong ShopĐối chiếu Product ID ở Play Console với SKU trong Shop
409 account_mismatchGiao dịch thuộc người chơi khác (obfuscatedAccountId ≠ player_uid)Bình thường (chống dùng lại token); app phải mua bằng đúng người chơi
202 pendingGiao dịch đang chờ (thanh toán chậm)Game thử lại sau
Xác minh lỗi, log backend có 401/403 từ GoogleThiếu/sai file service account (hoặc backend không đọc được file), service account chưa được mời hoặc quyền chưa có hiệu lực, chưa bật Android Developer APIMục 4.2–4.4; chờ quyền có hiệu lực

Áp dụng cho Portal v1.2.2