Library
godot-iap
Library version
godot-iap 3.0.1 (also present on main at 77f91d963c17629b9b0a3c167cf47497ae3a621b)
Affected platform
iOS
Store
App Store
Environment
Godot 4.7.x, GDScript addon API, StoreKit 2 bridge
What happened?
Any iOS call routed through _call_ios_async() can wait forever if the native bridge returns a request ID but the matching products_fetched completion is never emitted.
_await_products_fetched_for() contains an unbounded while true loop and awaits only the signal. It has no timeout, cancellation on disconnect/tree exit, or terminal plugin-state check:
This can strand product loading, restore, finish, verification, or other StoreKit-backed UI in a permanent busy state after a lost callback, plugin disconnect, app lifecycle interruption, or native failure that does not emit the completion.
Expected behavior
Every dispatched iOS request should resolve exactly once with either its matching native result or a bounded failure. Pending requests should be cancelled when the connection ends or the node exits the tree, and late callbacks should be ignored safely.
A default timeout plus a longer restore-specific timeout would be reasonable, provided callers receive a structured error such as timeout or service-disconnected.
Reproduction steps
- Use a test double for the iOS native plugin that returns a valid pending JSON payload containing a
requestId.
- Do not emit a matching
products_fetched signal (equivalent to a lost native completion).
- Call any API routed through
_call_ios_async(), such as fetch_products() or restore_purchases().
- Observe that the returned coroutine never resumes.
- Disconnect/end the IAP connection or remove the node from the tree.
- Observe that the waiter is still not resolved or cancelled.
Suggested regression coverage
- A matching
(method, requestId) completion resolves the intended waiter.
- A missing completion resolves with a timeout error.
- Disconnect/tree-exit cancels all pending requests.
- A late callback after timeout does not satisfy a newer request or grow the result cache indefinitely.
- Concurrent requests cannot consume each other's completions.
Logs or stack trace
No error is emitted. Execution remains suspended at:
var payload = await products_fetched
Additional context
This is a lifecycle/contract issue in the shared Godot wrapper rather than an app-specific timeout policy. A bounded native-operation contract would let apps reliably leave their purchasing/restoring state.
Library
godot-iap
Library version
godot-iap 3.0.1 (also present on
mainat77f91d963c17629b9b0a3c167cf47497ae3a621b)Affected platform
iOS
Store
App Store
Environment
Godot 4.7.x, GDScript addon API, StoreKit 2 bridge
What happened?
Any iOS call routed through
_call_ios_async()can wait forever if the native bridge returns a request ID but the matchingproducts_fetchedcompletion is never emitted._await_products_fetched_for()contains an unboundedwhile trueloop and awaits only the signal. It has no timeout, cancellation on disconnect/tree exit, or terminal plugin-state check:This can strand product loading, restore, finish, verification, or other StoreKit-backed UI in a permanent busy state after a lost callback, plugin disconnect, app lifecycle interruption, or native failure that does not emit the completion.
Expected behavior
Every dispatched iOS request should resolve exactly once with either its matching native result or a bounded failure. Pending requests should be cancelled when the connection ends or the node exits the tree, and late callbacks should be ignored safely.
A default timeout plus a longer restore-specific timeout would be reasonable, provided callers receive a structured error such as
timeoutorservice-disconnected.Reproduction steps
requestId.products_fetchedsignal (equivalent to a lost native completion)._call_ios_async(), such asfetch_products()orrestore_purchases().Suggested regression coverage
(method, requestId)completion resolves the intended waiter.Logs or stack trace
Additional context
This is a lifecycle/contract issue in the shared Godot wrapper rather than an app-specific timeout policy. A bounded native-operation contract would let apps reliably leave their purchasing/restoring state.