diff --git a/libraries/godot-iap/Example/tests/test_envelope_parsing.gd b/libraries/godot-iap/Example/tests/test_envelope_parsing.gd index 962480244..ee9f8d3ef 100644 --- a/libraries/godot-iap/Example/tests/test_envelope_parsing.gd +++ b/libraries/godot-iap/Example/tests/test_envelope_parsing.gd @@ -126,6 +126,12 @@ class FakeImmediateIOSPlugin: JSON.stringify({"success": true}) ) + func restorePurchases() -> String: + return responses.get( + "restorePurchases", + JSON.stringify({"success": true}) + ) + func _init() -> void: _run_suite.call_deferred() @@ -161,6 +167,7 @@ func _run_all_tests() -> void: await test_products_fetched_cache() await test_ios_async_timeout_and_late_callback() await test_ios_async_disconnect_and_concurrency() + await test_ios_restore_failure_emits_purchase_error() test_android_signal_handlers_parse_json() # Android JSON envelopes @@ -564,6 +571,49 @@ func test_android_request_purchase_success_envelope() -> void: _uninstall_fake() +## The non-iOS restore path reports failures through purchase_error. The iOS +## path used to return success = false silently, so a caller listening only to +## the signal saw Android restore failures but never iOS ones. +func test_ios_restore_failure_emits_purchase_error() -> void: + var previous_platform = GodotIapPlugin._platform + var previous_plugin = GodotIapPlugin._native_plugin + GodotIapPlugin._ios_async_results.clear() + GodotIapPlugin._ios_async_result_order.clear() + GodotIapPlugin._ios_async_terminal_keys.clear() + GodotIapPlugin._ios_async_terminal_order.clear() + + var errors: Array[Dictionary] = [] + var capture_error = func(error: Dictionary) -> void: + errors.append(error) + GodotIapPlugin.purchase_error.connect(capture_error) + + # The fake returns its payload without a requestId, so `_call_ios_async` + # takes the immediate-payload path and `restore_purchases()` runs its real + # iOS branch rather than the test re-implementing it. + var fake = _install_ios_fake() + fake.responses["restorePurchases"] = JSON.stringify({ + "success": false, + "code": "network-error", + "error": "Restore failed while offline", + }) + + var ios_result = await GodotIapPlugin.restore_purchases() + + _assert_false(ios_result.success, "A rejected iOS restore should not report success") + _assert_equal(errors.size(), 1, "A failed iOS restore should emit purchase_error") + _assert_equal(errors[0].get("code"), "network-error", "The native restore error code should be preserved") + _assert_equal( + errors[0].get("message"), + "Restore failed while offline", + "The native restore error message should be preserved" + ) + + GodotIapPlugin.purchase_error.disconnect(capture_error) + _uninstall_fake() + GodotIapPlugin._platform = previous_platform + GodotIapPlugin._native_plugin = previous_plugin + + func test_android_request_purchase_error_envelope() -> void: var fake = _install_android_fake() fake.responses["requestPurchase"] = JSON.stringify({ diff --git a/libraries/godot-iap/Makefile b/libraries/godot-iap/Makefile index 03779b577..31a0e870e 100644 --- a/libraries/godot-iap/Makefile +++ b/libraries/godot-iap/Makefile @@ -180,7 +180,10 @@ macos-build: # Build Android plugin android: setup gradle-wrapper @echo "$(GREEN)Building Android plugin...$(NC)" - @cd $(ANDROID_DIR) && ./gradlew copyReleaseAarToAddons + @# export-android runs --export-debug, and the export plugin embeds the + @# debug AAR for debug exports. Refresh BOTH variants so a debug export can + @# never ship a stale plugin while only the release AAR gets rebuilt. + @cd $(ANDROID_DIR) && ./gradlew copyDebugAarToAddons copyReleaseAarToAddons @echo "$(GREEN)Generating GDAP file...$(NC)" @$(PROJECT_ROOT)/scripts/write-gdap.sh "$(ADDON_DIR)/android/GodotIap.gdap" @echo "$(GREEN)✓ Android plugin built$(NC)" diff --git a/libraries/godot-iap/addons/godot-iap/android/GodotIap.debug.aar b/libraries/godot-iap/addons/godot-iap/android/GodotIap.debug.aar index 3e85d5f0b..824759836 100644 Binary files a/libraries/godot-iap/addons/godot-iap/android/GodotIap.debug.aar and b/libraries/godot-iap/addons/godot-iap/android/GodotIap.debug.aar differ