Skip to main content
Version: 1.1

Signals (Listeners)

IAPKit - In-App Purchase Solution

GodotIap provides signals to handle purchase updates and errors. These signals are essential for handling the asynchronous nature of in-app purchases.

purchase_updated​

Emitted when a purchase is successfully completed or updated.

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.purchase_updated.connect(_on_purchase_updated)

func _on_purchase_updated(purchase: Dictionary):
print("Purchase received:", purchase)

var state = purchase.get("purchaseState", "")
var product_id = purchase.productId

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

if is_valid:
# Grant purchase to user
grant_purchase_to_user(purchase)

# Finish the transaction
finish_purchase(purchase)

print("Purchase completed successfully")
else:
print("Purchase verification failed")

"pending":
print("Purchase pending approval")

_:
print("Unknown purchase state: ", state)

Signal Parameters:

  • purchase (Dictionary): The purchase object containing:
    • productId (String): Product identifier
    • transactionId (String): Transaction identifier
    • purchaseToken (String): Token for verification
    • purchaseState (String): "purchased", "pending", or "unspecified"
    • Platform-specific fields (see Purchase Interface)

purchase_error​

Emitted when a purchase error occurs.

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.purchase_error.connect(_on_purchase_error)

func _on_purchase_error(error: Dictionary):
print("Purchase error:", error)

var code = error.get("code", "")
var message = error.get("message", "")

match code:
"USER_CANCELED":
# User cancelled the purchase - don't show error
print("Purchase cancelled by user")

"NETWORK_ERROR":
show_error_message("Network error. Please check your connection.")

"ITEM_UNAVAILABLE":
show_error_message("This product is currently unavailable.")

"ITEM_ALREADY_OWNED":
# User already owns this product - restore it
show_error_message("You already own this product.")
restore_purchases()

"PAYMENT_INVALID":
show_error_message("Payment was invalid. Please try again.")

"PAYMENT_NOT_ALLOWED":
show_error_message("Payments are not allowed on this device.")

_:
show_error_message("Purchase failed: " + message)

Signal Parameters:

  • error (Dictionary): The error object containing:
    • code (String): Error code
    • message (String): Human-readable error message
    • domain (String): Error domain (iOS only)

Common Error Codes:

CodeDescription
USER_CANCELEDUser cancelled the purchase
NETWORK_ERRORNetwork connection error
ITEM_UNAVAILABLEProduct not available
ITEM_ALREADY_OWNEDUser already owns this non-consumable
ITEM_NOT_OWNEDItem not owned (for consume/acknowledge)
PAYMENT_INVALIDPayment information is invalid
PAYMENT_NOT_ALLOWEDPayments disabled on device
BILLING_UNAVAILABLEBilling service unavailable

products_fetched​

Emitted when products are successfully fetched.

var products: Array = []

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.products_fetched.connect(_on_products_fetched)

# Fetch products
var product_ids = ["coins_100", "coins_500"]
iap.fetch_products(JSON.stringify(product_ids), "inapp")

func _on_products_fetched(fetched_products: Array):
products = fetched_products

for product in products:
print("Product: ", product.productId)
print("Title: ", product.title)
print("Price: ", product.localizedPrice)
print("---")

update_store_ui()

Signal Parameters:

  • products (Array): Array of product dictionaries

subscriptions_fetched​

Emitted when subscriptions are successfully fetched.

var subscriptions: Array = []

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.subscriptions_fetched.connect(_on_subscriptions_fetched)

# Fetch subscriptions
var sub_ids = ["premium_monthly", "premium_yearly"]
iap.fetch_subscriptions(JSON.stringify(sub_ids))

func _on_subscriptions_fetched(fetched_subs: Array):
subscriptions = fetched_subs

for sub in subscriptions:
print("Subscription: ", sub.productId)
print("Price: ", sub.localizedPrice)

Signal Parameters:

  • subscriptions (Array): Array of subscription dictionaries

iOS-Specific Signals​

Emitted when a user initiates a promoted product purchase from the App Store.

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.promoted_product_ios.connect(_on_promoted_product)

func _on_promoted_product(product_id: String):
print("Promoted product purchase initiated: ", product_id)

# Show your custom purchase confirmation UI
var confirmed = await show_product_confirmation(product_id)

if confirmed:
# Complete the promoted purchase
iap.request_purchase_on_promoted_product_ios()

Signal Parameters:

  • product_id (String): The product ID of the promoted product

Related Methods:

  • get_promoted_product_ios(): Get the promoted product details
  • request_purchase_on_promoted_product_ios(): Complete the promoted product purchase
note

This signal only fires on iOS devices when a user taps on a promoted product in the App Store.


subscription_status_changed_ios​

Emitted when a subscription status changes (iOS only).

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.subscription_status_changed_ios.connect(_on_subscription_status_changed)

func _on_subscription_status_changed(status: Dictionary):
print("Subscription status changed")
print("Product ID: ", status.productId)
print("State: ", status.state)

match status.state:
"subscribed":
grant_premium_access()
"expired":
revoke_premium_access()
"inBillingRetryPeriod":
show_billing_issue_banner()
"inGracePeriod":
show_grace_period_banner()

Android-Specific Signals​

user_choice_billing_android​

Emitted when user chooses alternative billing in the User Choice Billing dialog (Android only).

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.user_choice_billing_android.connect(_on_user_choice_billing)

func _on_user_choice_billing(data: Dictionary):
print("User selected alternative billing")
print("Token: ", data.externalTransactionToken)
print("Products: ", data.products)

# Process payment in your payment system
var payment_result = await process_external_payment(data.products)

if payment_result.success:
# Report token to Google Play within 24 hours
await report_token_to_google(data.externalTransactionToken)

Signal Parameters:

  • data (Dictionary):
    • externalTransactionToken (String): Token to report to Google
    • products (Array): List of product IDs
Important

The externalTransactionToken must be reported to Google Play backend within 24 hours.


developer_provided_billing_android​

Emitted when user selects developer billing option in External Payments flow (Android only, Japan).

func _ready():
if Engine.has_singleton("GodotIap"):
var iap = Engine.get_singleton("GodotIap")
iap.developer_provided_billing_android.connect(_on_developer_billing)

func _on_developer_billing(data: Dictionary):
print("User selected developer billing")
print("Token: ", data.externalTransactionToken)

# Process payment with your gateway
var payment_result = await process_payment_with_gateway(
data.externalTransactionToken
)

if payment_result.success:
# Report to Google Play within 24 hours
await report_external_transaction_to_google(
data.externalTransactionToken
)

Signal Parameters:

  • data (Dictionary):
    • externalTransactionToken (String): Token to report to Google

Requirements:

  • Google Play Billing Library 8.3.0+
  • Only available in Japan

Setting Up All Signals​

Here's a complete example of setting up all purchase signals:

extends Node

var iap: GodotIap
var products: Array = []
var subscriptions: Array = []

func _ready():
if not Engine.has_singleton("GodotIap"):
print("GodotIap not available")
return

iap = Engine.get_singleton("GodotIap")
_setup_signals()
_initialize()

func _setup_signals():
# Core signals
iap.purchase_updated.connect(_on_purchase_updated)
iap.purchase_error.connect(_on_purchase_error)
iap.products_fetched.connect(_on_products_fetched)
iap.subscriptions_fetched.connect(_on_subscriptions_fetched)

# iOS-specific signals
if OS.get_name() == "iOS":
iap.promoted_product_ios.connect(_on_promoted_product)
iap.subscription_status_changed_ios.connect(_on_subscription_status_changed)

# Android-specific signals
if OS.get_name() == "Android":
iap.user_choice_billing_android.connect(_on_user_choice_billing)
iap.developer_provided_billing_android.connect(_on_developer_billing)

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

func _load_products():
var product_ids = ["coins_100", "remove_ads"]
iap.fetch_products(JSON.stringify(product_ids), "inapp")

var sub_ids = ["premium_monthly"]
iap.fetch_subscriptions(JSON.stringify(sub_ids))

# Core signal handlers
func _on_purchase_updated(purchase: Dictionary):
print("Purchase updated: ", purchase.productId)
await handle_purchase(purchase)

func _on_purchase_error(error: Dictionary):
print("Purchase error: ", error.code)
handle_error(error)

func _on_products_fetched(fetched_products: Array):
products = fetched_products
update_product_ui()

func _on_subscriptions_fetched(fetched_subs: Array):
subscriptions = fetched_subs
update_subscription_ui()

# iOS signal handlers
func _on_promoted_product(product_id: String):
print("Promoted product: ", product_id)

func _on_subscription_status_changed(status: Dictionary):
print("Subscription status: ", status.state)

# Android signal handlers
func _on_user_choice_billing(data: Dictionary):
print("User choice billing")

func _on_developer_billing(data: Dictionary):
print("Developer billing")

# Helper functions
func handle_purchase(purchase: Dictionary):
var verified = await verify_on_server(purchase)
if verified:
grant_purchase(purchase.productId)
finish_transaction(purchase)

func handle_error(error: Dictionary):
if error.code != "USER_CANCELED":
show_error(error.message)

Signal Lifecycle Notes​

1. Set up early​

Set up signals as early as possible in your app lifecycle to catch any pending purchases.

2. Clean up properly​

Signals in Godot are automatically cleaned up when the node is freed, but you can manually disconnect if needed:

func _exit_tree():
if iap:
iap.purchase_updated.disconnect(_on_purchase_updated)
iap.purchase_error.disconnect(_on_purchase_error)

3. Handle app states​

Purchases can complete when your app is in the background. Always handle the purchase_updated signal to process any pending purchases when the app resumes.

4. Purchase States​

Purchases can be in different states:

StateDescription
purchasedSuccessfully completed
pendingAwaiting approval (e.g., parental approval)
unspecifiedUnknown state

Handle each state appropriately in your purchase signal handler.