Plugin
IAP
Mua hàng trong game với Google Play Billing 9 (Android) và StoreKit (iOS) — cùng một API, kết nối store tự chạy khi app mở.
IAP
Addon leono_iap
Gồm thư viện Android. Thư viện iOS chưa có trong gói (cần build trên Mac) — trên iOS các hàm trả kết quả rỗng. Giải nén vào thư mục gốc project Godot.
Tạo sản phẩm trên store
- Play Console → app → Monetize with Play → Products.
- In-app products: gem, coin (tiêu hao) và remove ads (mua một lần). Ví dụ ID:
gem_100,remove_ads. - Subscriptions: gói VIP, mỗi gói cần ít nhất một base plan đang active.
- App phải được upload ít nhất lên Internal testing thì Billing mới trả sản phẩm.
- Test không mất tiền: Settings → License testing → thêm Gmail người test.
- App Store Connect → app → Monetization → In-App Purchases / Subscriptions. Nên dùng cùng Product ID với Android.
- Bật capability In-App Purchase cho App ID.
- Test bằng tài khoản Sandbox (Users and Access → Sandbox).
Cài plugin
- Build
build_android.bat iap; iOS trên Mac:scripts/build_ios.sh iap. - Copy
plugins/iap/addon/leono_iap/vàores://addons/. - Bật pluginProject Settings → Plugins → Leono IAP. Autoload
IAPtự được thêm. - ExportAndroid: Use Gradle Build (permission
BILLINGtự thêm). iOS: export bình thường,StoreKittự được link.
Bắt đầu
const GEMS := {"gem_100": 100, "gem_550": 550}
func _ready() -> void:
IAP.purchase_updated.connect(_deliver) # nối sớm
var products := await IAP.query_products(GEMS.keys() + ["remove_ads"], ["vip_monthly"])
for id in products:
shop.set_price(id, products[id].price)
func buy(id: String) -> void:
var r := await IAP.purchase(id)
match r.status:
"purchased": await _deliver(r.purchase)
"pending": show_message("Thanh toán đang chờ xử lý")
"error": show_message("Không mua được: " + r.error)
func _deliver(p: Dictionary) -> void:
if p.state != "purchased":
return
if GEMS.has(p.product_id):
add_gems(GEMS[p.product_id])
await IAP.consume(p) # tiêu hao
else:
unlock(p.product_id)
await IAP.acknowledge(p) # mua một lần / subscription
Giao vật phẩm trước, rồi mới consume() / acknowledge(). Nếu app crash giữa chừng, purchase chưa hoàn tất sẽ được store gửi lại qua purchase_updated hoặc restore_purchases() để giao tiếp — người chơi không bị mất tiền oan.
Cấu hình
Không có Project Setting. Tuỳ chọn duy nhất là IAP.simulate_purchases để test trong editor.
IAP.simulate_purchases = not OS.has_feature("mobile") # editor/PC: mua giả luôn thành công
API reference
Android: đã kết nối Google Play. iOS: máy cho phép thanh toán (không bị Screen Time chặn). Các hàm khác tự chờ kết nối tối đa 5 giây nên thường không cần gọi.
Lấy thông tin và giá theo tiền tệ của người chơi. Phải gọi trước purchase(). ID không có trong kết quả là chưa tạo hoặc chưa active trên store.
| Key | Kiểu | Mô tả |
|---|---|---|
id, title, description | String | |
type | String | "inapp" / "subs" |
price | String | Đã định dạng, ví dụ "25.000 ₫" — hiển thị thẳng |
price_micros, currency | int, String | Giá × 1.000.000 và mã tiền tệ — dùng cho analytics |
billing_period | String | Chỉ subscription, ISO 8601: P1W, P1M, P1Y |
Mở hộp thoại thanh toán của store. Trả {status, purchase, error}. Mỗi lúc chỉ một giao dịch.
| status | Ý nghĩa |
|---|---|
purchased | Đã thanh toán — giao vật phẩm rồi consume/acknowledge |
pending | Chờ thanh toán (tiền mặt ở cửa hàng, Ask to Buy) — chưa giao; kết quả đến sau qua purchase_updated |
cancelled | Người chơi đóng hộp thoại |
error | Xem error: product_not_loaded, purchase_in_progress, already_owned, billing_unavailable, network_error, service_unavailable, item_unavailable, developer_error, not_available |
already_owned (Android): người chơi đã mua nhưng game chưa consume — gọi restore_purchases() rồi giao + consume.
Cho item tiêu hao (gem, coin, lượt chơi): hoàn tất giao dịch để mua lại được.
Cho item mua một lần (remove ads, mở khoá) và subscription. Purchase đã acknowledge rồi thì trả true ngay.
Android: purchase không được acknowledge trong 3 ngày sẽ bị Google tự hoàn tiền và thu hồi.
Các purchase đang sở hữu. Android: in-app đang sở hữu (gồm cả chưa consume) + subscription active — không hiện gì, gọi lúc mở game được. iOS: non-consumable + subscription (Apple không khôi phục item tiêu hao).
iOS có thể hỏi mật khẩu Apple ID — chỉ gọi từ nút "Khôi phục giao dịch". Apple bắt buộc game bán non-consumable phải có nút này.
Purchase hoàn tất ngoài lời gọi purchase(): thanh toán pending vừa xong, mua từ trang store/promo code, giao dịch chưa hoàn tất từ lần chạy trước (iOS gửi lại khi mở app). Nối signal này sớm, xử lý giống kết quả purchased.
Purchase
| Key | Android | iOS |
|---|---|---|
product_id | Product ID | |
state | "purchased" / "pending" | |
token | Purchase token | Transaction ID |
order_id | GPA.xxxx | Original transaction ID |
acknowledged | Đã acknowledge chưa | Luôn false |
quantity, time | Số lượng; thời điểm mua (ms) | |
signature, original_json | Để server xác thực | "" |
receipt | "" | App receipt base64 để server xác thực với Apple |
Hướng dẫn: xác thực phía server
Plugin không tự xác thực giao dịch. Game có kinh tế quan trọng (PvP, giao dịch giữa người chơi) nên gửi purchase lên server và chỉ giao hàng khi server xác nhận:
- Android: gửi
product_id+token; server gọi Google Play Developer APIpurchases.products.get/purchases.subscriptionsv2.get. - iOS: gửi
receipt(hoặcorder_id); server kiểm tra với App Store Server API.
Khác biệt giữa nền tảng
| Android | iOS | |
|---|---|---|
| Kết nối | Tự kết nối và tự kết nối lại | Không cần |
consume / acknowledge | Khác nhau | Như nhau (finish transaction) |
restore_purchases() | Không hiện gì | Có thể hỏi mật khẩu Apple ID |
| Subscription có nhiều offer | Dùng base plan đầu tiên | Theo sản phẩm |
| API store | Play Billing 9.1.0 | StoreKit 1 (deprecated từ iOS 18, vẫn chạy) |
Code iOS chưa được build — cần máy Mac chạy scripts/build_ios.sh iap và test bằng tài khoản Sandbox.
Lịch sử phiên bản
| Phiên bản | Thay đổi |
|---|---|
1.0.0 | Phát hành đầu tiên: Play Billing 9.1.0, StoreKit 1. |