Library
godot-iap
Library version
godot-iap 3.0.1 (also present on main at 77f91d963c17629b9b0a3c167cf47497ae3a621b)
Affected platform
Android and iOS
Store
Google Play Store and App Store
Environment
Godot 4.7.x, GDScript addon API
What happened?
The public request_purchase() contract tells callers to rely on purchase_updated / purchase_error, not the return value. However, several terminal failure paths return null without emitting purchase_error:
- both
requestPurchase and requestSubscription are present;
- neither request is present;
- the platform payload is not a dictionary;
- the requested type does not match the request kind;
- the Android Google payload is invalid;
- the iOS Apple payload is invalid or has an empty SKU;
- the platform is unsupported;
- the native response is malformed JSON.
The relevant implementation and its event-based API documentation are here:
An event-driven purchase screen that enters a busy state before calling request_purchase() can therefore remain stuck forever: it receives neither purchase_updated nor purchase_error, and the documented return value is intentionally not the outcome channel.
Expected behavior
After request_purchase() is invoked, every terminal failure should emit one normalized purchase_error payload. Synchronous validation, unsupported-platform, empty native response, native failure response, and parse failure paths should follow the same event contract.
Alternatively, the public API could return a documented dispatch result, but the current guidance explicitly says not to rely on the return value. Under that contract, exactly one terminal event is the least surprising behavior.
Reproduction steps
- Connect handlers to
purchase_updated and purchase_error.
- Construct props containing both
requestPurchase and requestSubscription (or use an empty iOS SKU).
- Call
await request_purchase(props).
- Observe that it returns
null.
- Observe that neither signal is emitted, so an event-driven caller has no terminal notification.
The malformed-native-response path has the same behavior: _request_purchase_raw() returns { success: false, error: "Failed to parse response" }, then the public wrapper returns null without emitting purchase_error.
Suggested regression coverage
- Each synchronous validation failure emits exactly one
purchase_error.
- Empty and malformed native responses emit exactly one
purchase_error.
- A native
{ success: false } result remains exactly-once (no duplicate error event).
- Successful dispatch still completes through the existing purchase events.
Logs or stack trace
request_purchase(...) -> null
purchase_updated emissions -> 0
purchase_error emissions -> 0
Additional context
Normalizing these paths upstream prevents every Godot consumer from adding its own purchase-state watchdog and duplicate error adapter.
Library
godot-iap
Library version
godot-iap 3.0.1 (also present on
mainat77f91d963c17629b9b0a3c167cf47497ae3a621b)Affected platform
Android and iOS
Store
Google Play Store and App Store
Environment
Godot 4.7.x, GDScript addon API
What happened?
The public
request_purchase()contract tells callers to rely onpurchase_updated/purchase_error, not the return value. However, several terminal failure paths returnnullwithout emittingpurchase_error:requestPurchaseandrequestSubscriptionare present;The relevant implementation and its event-based API documentation are here:
An event-driven purchase screen that enters a busy state before calling
request_purchase()can therefore remain stuck forever: it receives neitherpurchase_updatednorpurchase_error, and the documented return value is intentionally not the outcome channel.Expected behavior
After
request_purchase()is invoked, every terminal failure should emit one normalizedpurchase_errorpayload. Synchronous validation, unsupported-platform, empty native response, native failure response, and parse failure paths should follow the same event contract.Alternatively, the public API could return a documented dispatch result, but the current guidance explicitly says not to rely on the return value. Under that contract, exactly one terminal event is the least surprising behavior.
Reproduction steps
purchase_updatedandpurchase_error.requestPurchaseandrequestSubscription(or use an empty iOS SKU).await request_purchase(props).null.The malformed-native-response path has the same behavior:
_request_purchase_raw()returns{ success: false, error: "Failed to parse response" }, then the public wrapper returnsnullwithout emittingpurchase_error.Suggested regression coverage
purchase_error.purchase_error.{ success: false }result remains exactly-once (no duplicate error event).Logs or stack trace
Additional context
Normalizing these paths upstream prevents every Godot consumer from adding its own purchase-state watchdog and duplicate error adapter.