Skip to main content
Version: 1.0

Troubleshooting

IAPKit - In-App Purchase Solution

This guide covers common issues you might encounter when implementing in-app purchases with GodotIap and how to resolve them.

Prerequisites Checklist​

Before diving into troubleshooting, ensure you have completed these essential steps:

App Store Setup (iOS)​

  • Agreements: Completed all agreements, tax, and banking information in App Store Connect
  • Sandbox Account: Created sandbox testing accounts in "Users and Roles"
  • Device Setup: Signed into iOS device with sandbox account
  • Products Created: Set up In-App Purchase products with status "Ready to Submit"
  • StoreKit Capability: Added StoreKit capability in Xcode

Google Play Setup (Android)​

  • Play Console: Completed all required information in Google Play Console
  • Test Accounts: Added test accounts to your app's testing track
  • Signed Build: Using signed APK/AAB (not debug builds)
  • Upload: Uploaded at least one version to internal testing
  • Products Active: Products are set to "Active" status

Common Issues​

fetch_products() Returns Empty Array​

This is one of the most common issues. Here are the potential causes and solutions:

1. Connection Not Established​

var is_connected: bool = false

func _initialize():
var result = JSON.parse_string(iap.init_connection())

if result.get("success", false):
is_connected = true
# Only fetch products AFTER successful connection
_load_products()
else:
print("Connection failed: ", result.get("error", ""))

func _load_products():
if not is_connected:
print("Cannot fetch products - not connected")
return

var product_ids = ["coins_100", "premium"]
iap.fetch_products(JSON.stringify(product_ids), "inapp")

2. Product IDs Don't Match​

Ensure your product IDs exactly match those configured in the stores:

# Wrong: Using different IDs
var product_ids = ["my_product_1", "my_product_2"]

# Correct: Using exact IDs from store
var product_ids = ["com.yourapp.coins_100", "com.yourapp.premium"]

3. Products Not Approved (iOS)​

Products need time to propagate through Apple's systems:

  • Wait up to 24 hours after creating products
  • Ensure products are in "Ready to Submit" status
  • Test with sandbox accounts

4. App Not Uploaded to Play Console (Android)​

For Android, your app must be uploaded to Play Console:

# Export signed APK from Godot
# Upload to Play Console internal testing track

Plugin Not Found​

1. Check if Singleton Exists​

func _ready():
if not Engine.has_singleton("GodotIap"):
print("GodotIap plugin not available!")
print("Make sure the plugin is enabled in export settings")
return

iap = Engine.get_singleton("GodotIap")

2. Verify Export Settings​

Android:

  1. Go to Project → Export
  2. Select Android preset
  3. Scroll to "Plugins"
  4. Ensure "GodotIap" is checked

iOS:

  1. Build the plugin using swift build
  2. Copy framework to your Xcode project
  3. Ensure framework is linked

Purchase Flow Issues​

1. Purchases Not Completing​

Always handle purchase updates and finish transactions:

func _on_purchase_updated(purchase: Dictionary):
var state = purchase.get("purchaseState", "")

if state == "purchased":
# Validate receipt on your server
var is_valid = await validate_on_server(purchase)

if is_valid:
# Grant purchase to user
grant_purchase(purchase.productId)

# ALWAYS finish the transaction
var params = {
"purchase": purchase,
"isConsumable": is_consumable(purchase.productId)
}
iap.finish_transaction(JSON.stringify(params))

Important - Transaction Acknowledgment Requirements:

  • iOS: Unfinished transactions remain in the queue indefinitely until finish_transaction is called
  • Android: Purchases must be acknowledged within 3 days (72 hours) or they will be automatically refunded

2. purchase_updated Signal Triggering on App Restart​

This happens when transactions are not properly finished. The store replays unfinished transactions on app startup.

Problem: Your purchase_updated signal fires automatically every time the app starts.

Cause: You didn't call finish_transaction after processing the purchase.

Solution: Always call finish_transaction after successfully processing:

func _on_purchase_updated(purchase: Dictionary):
if purchase.purchaseState == "purchased":
# 1. Validate on server
var is_valid = await validate_on_server(purchase)
if not is_valid:
print("Invalid purchase")
return

# 2. Process the purchase
await process_purchase(purchase)

# 3. FINISH THE TRANSACTION
var params = {
"purchase": purchase,
"isConsumable": is_consumable(purchase.productId)
}
iap.finish_transaction(JSON.stringify(params))

Handle pending transactions on startup:

func _ready():
if Engine.has_singleton("GodotIap"):
iap = Engine.get_singleton("GodotIap")
_setup_signals()
_initialize()

func _initialize():
var result = JSON.parse_string(iap.init_connection())
if result.get("success", false):
# Check for pending purchases FIRST
_check_pending_purchases()
# Then load products
_load_products()

func _check_pending_purchases():
var purchases = JSON.parse_string(iap.get_available_purchases())

for purchase in purchases:
if await is_already_processed(purchase):
# Just finish the transaction
var params = {"purchase": purchase, "isConsumable": false}
iap.finish_transaction(JSON.stringify(params))
else:
# Process then finish
await process_purchase(purchase)
var params = {"purchase": purchase, "isConsumable": is_consumable(purchase.productId)}
iap.finish_transaction(JSON.stringify(params))

Connection Issues​

1. Network Connectivity​

Handle network errors gracefully:

func _initialize():
var result = JSON.parse_string(iap.init_connection())

if not result.get("success", false):
var error = result.get("error", "")

if "network" in error.to_lower():
show_retry_dialog("Network error. Please check your connection.")
else:
show_error("Failed to connect to store: " + error)

func show_retry_dialog(message: String):
# Show dialog with retry button
# On retry: call _initialize() again
pass

2. Store Service Unavailable​

Sometimes store services are temporarily unavailable:

func handle_store_unavailable():
var dialog = AcceptDialog.new()
dialog.title = "Store Unavailable"
dialog.dialog_text = "The store is temporarily unavailable. Please try again later."
add_child(dialog)
dialog.popup_centered()

Platform-Specific Issues​

iOS Issues​

  1. Invalid Product ID Error
# Ensure:
# - Signed in with sandbox account on device
# - Product IDs match exactly (case-sensitive)
# - App bundle ID matches App Store Connect
# - Products are in "Ready to Submit" status
  1. StoreKit Configuration
# In Xcode:
# - Add StoreKit capability
# - For iOS 15+, ensure SwiftUI is available
  1. Sandbox Testing
# For iOS sandbox testing:
# 1. Sign out of App Store on device
# 2. Don't sign in until prompted during purchase
# 3. Use sandbox account credentials when prompted

Android Issues​

  1. Billing Client Setup
// android/build.gradle.kts
dependencies {
implementation("com.android.billingclient:billing:7.0.0")
}
  1. License Testing
# For Android testing:
# 1. Add your test account email to Google Play Console
# 2. Go to Settings → License testing
# 3. Add the Gmail accounts
# 4. Testers can purchase without being charged
  1. Version Code Issues
# Ensure:
# - App is uploaded to Play Console (at least internal track)
# - Version code matches or exceeds uploaded version
# - Test device uses account added to testing track
  1. Service Disconnected
func _on_purchase_error(error: Dictionary):
if error.get("code") == "SERVICE_DISCONNECTED":
# Attempt to reconnect
var result = JSON.parse_string(iap.init_connection())
if result.get("success", false):
print("Reconnected to Google Play")

Debugging Tips​

1. Enable Verbose Logging​

func _on_purchase_updated(purchase: Dictionary):
print("=== Purchase Updated ===")
print(JSON.stringify(purchase, "\t"))
print("========================")

func _on_purchase_error(error: Dictionary):
print("=== Purchase Error ===")
print(JSON.stringify(error, "\t"))
print("======================")

func _on_products_fetched(products: Array):
print("=== Products Fetched ===")
for product in products:
print(JSON.stringify(product, "\t"))
print("========================")

2. Monitor Connection State​

var is_connected: bool = false

func _initialize():
var result = JSON.parse_string(iap.init_connection())
is_connected = result.get("success", false)

print("Connection state: ", "Connected" if is_connected else "Disconnected")

if not is_connected:
print("Connection error: ", result.get("error", "Unknown"))

3. Check Device Logs​

Android (adb logcat):

adb logcat | grep -i "GodotIap\|BillingClient\|Purchase"

iOS (Xcode Console): Look for StoreKit-related logs in Xcode's debug console.

Testing Strategies​

1. Staged Testing Approach​

  1. Development: Test IAP logic without actual store calls
  2. Sandbox Testing: Use store sandbox/test accounts
  3. Internal Testing: Test with real store in closed testing
  4. Production: Final verification in live environment

2. Test Different Scenarios​

# Test scenarios to cover:
var test_scenarios = [
"successful_purchase",
"user_cancelled",
"network_error",
"item_already_owned",
"product_unavailable",
"pending_purchase",
"restore_purchases"
]

3. Device Testing Matrix​

Test on various devices:

  • iOS: Different iPhone/iPad models, iOS versions
  • Android: Different manufacturers, Android versions, Play Services versions

Error Code Reference​

CodeDescriptionRecommended Action
USER_CANCELEDUser cancelled purchaseNo action needed
NETWORK_ERRORNetwork connectivity issueShow retry option
ITEM_UNAVAILABLEProduct not availableCheck product setup
ITEM_ALREADY_OWNEDUser already owns productRestore purchase
ITEM_NOT_OWNEDItem not ownedCheck purchase status
PAYMENT_INVALIDPayment method issueDirect to settings
PAYMENT_NOT_ALLOWEDPayments disabledShow message
BILLING_UNAVAILABLEStore unavailableShow retry later
DEVELOPER_ERRORConfiguration errorCheck setup

Getting Help​

If you're still experiencing issues:

  1. Check logs: Review device logs and console output
  2. Search issues: Check the GitHub issues
  3. Minimal reproduction: Create a minimal example that reproduces the issue
  4. Report bug: File a detailed issue with reproduction steps

Bug Report Template​

**Environment:**
- godot-iap version: x.x.x
- Godot version: x.x.x
- Platform: iOS/Android
- OS version: x.x.x
- Device: Device model

**Description:**
Clear description of the issue

**Steps to reproduce:**
1. Step 1
2. Step 2
3. Step 3

**Expected behavior:**
What should happen

**Actual behavior:**
What actually happens

**Logs:**
Relevant logs and error messages

See Also​