Skip to content

[Bug] godot-iap iOS async calls can wait forever #271

Description

@hyochan

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

  1. Use a test double for the iOS native plugin that returns a valid pending JSON payload containing a requestId.
  2. Do not emit a matching products_fetched signal (equivalent to a lost native completion).
  3. Call any API routed through _call_ios_async(), such as fetch_products() or restore_purchases().
  4. Observe that the returned coroutine never resumes.
  5. Disconnect/end the IAP connection or remove the node from the tree.
  6. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    godot-iapgodot-iap library🐛 bugSomething isn't working📱 iOSRelated to iOS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions