From 093ed82558c9093fbfcfc95b57783bdd6d35fc58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:17:20 +0000 Subject: [PATCH 1/4] Initial plan From 69515d1de551c425c50e40e43430e10652565540 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:29:43 +0000 Subject: [PATCH 2/4] Fix iOS restore: defer handleRestore to allow pending transactions to arrive On iOS 26.4+, restoreCompletedTransactionsFinished may be called before all restored transactions are delivered via updatedTransactions. This caused handleRestore to be called with an empty array. Fix by deferring the handleRestore delivery using a 500ms timer, giving pending updatedTransactions callbacks time to process restored transactions first. Agent-Logs-Url: https://github.com/libgdx/gdx-pay/sessions/9dc875b1-3666-46a7-a485-0946d118e71b Co-authored-by: keesvandieren <863966+keesvandieren@users.noreply.github.com> --- .../ios/apple/PurchaseManageriOSApple.java | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java index f21b869..24fd418 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java @@ -21,6 +21,7 @@ import libcore.io.Base64; import org.robovm.apple.foundation.*; import org.robovm.apple.storekit.*; +import org.robovm.objc.block.VoidBlock1; import javax.annotation.Nullable; import java.util.ArrayList; @@ -52,6 +53,7 @@ public class PurchaseManageriOSApple implements PurchaseManager { private NSArray products; private final List restoredTransactions = new ArrayList(); + private NSTimer restoreTimer; @Override public String storeName () { @@ -107,6 +109,9 @@ public boolean installed () { @Override public void dispose () { if (appleObserver != null) { + // Cancel any pending restore delivery timer. + cancelRestoreTimer(); + // Remove and null our apple transaction observer. SKPaymentQueue defaultQueue = SKPaymentQueue.getDefaultQueue(); @@ -151,6 +156,8 @@ public void purchase (String identifier) { public void purchaseRestore () { log(LOGTYPELOG, "Restoring purchases..."); + // Cancel any pending restore delivery timer. + cancelRestoreTimer(); // Clear previously restored transactions. restoredTransactions.clear(); // Start the restore flow. @@ -483,16 +490,26 @@ else if (error.getCode() == SKErrorCode.PaymentCancelled.value()) { @Override public void restoreCompletedTransactionsFinished (SKPaymentQueue queue) { - // All products have been restored. log(LOGTYPELOG, "All transactions have been restored!"); - observer.handleRestore(restoredTransactions.toArray(new Transaction[restoredTransactions.size()])); - restoredTransactions.clear(); + // On iOS 26.4+, restoreCompletedTransactionsFinished may be called + // before all restored transactions are delivered via updatedTransactions. + // Defer delivery to allow any pending restored transactions to be processed. + cancelRestoreTimer(); + restoreTimer = NSTimer.createScheduled(0.5, false, new VoidBlock1() { + @Override + public void invoke(NSTimer timer) { + restoreTimer = null; + observer.handleRestore(restoredTransactions.toArray(new Transaction[restoredTransactions.size()])); + restoredTransactions.clear(); + } + }); } @Override public void restoreCompletedTransactionsFailed (SKPaymentQueue queue, NSError error) { // Restoration failed. + cancelRestoreTimer(); // Decide if user cancelled or transaction failed. if (error.getCode() == SKErrorCode.PaymentCancelled.value()) { @@ -505,6 +522,13 @@ public void restoreCompletedTransactionsFailed (SKPaymentQueue queue, NSError er } } + private void cancelRestoreTimer() { + if (restoreTimer != null) { + restoreTimer.invalidate(); + restoreTimer = null; + } + } + void log (final int type, final String message) { log(type, message, null); } From 739d5065f966d8a6f96feb3b7c649a6c72f03ffa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:31:32 +0000 Subject: [PATCH 3/4] Add rationale comment for 500ms timer delay Agent-Logs-Url: https://github.com/libgdx/gdx-pay/sessions/9dc875b1-3666-46a7-a485-0946d118e71b Co-authored-by: keesvandieren <863966+keesvandieren@users.noreply.github.com> --- .../com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java | 1 + 1 file changed, 1 insertion(+) diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java index 24fd418..9b5dd26 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java @@ -495,6 +495,7 @@ public void restoreCompletedTransactionsFinished (SKPaymentQueue queue) { // On iOS 26.4+, restoreCompletedTransactionsFinished may be called // before all restored transactions are delivered via updatedTransactions. // Defer delivery to allow any pending restored transactions to be processed. + // 500ms is chosen to safely exceed the observed ~60ms gap between callbacks. cancelRestoreTimer(); restoreTimer = NSTimer.createScheduled(0.5, false, new VoidBlock1() { @Override From a44088a25f39491175b7b21d91c3adea09771d11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:44:29 +0000 Subject: [PATCH 4/4] Switch from NSTimer to libGDX Timer for restore deferral Replaces the iOS-specific NSTimer (which didn't compile) with com.badlogic.gdx.utils.Timer, matching the approach suggested in issue #285. Agent-Logs-Url: https://github.com/libgdx/gdx-pay/sessions/042d60fa-727f-43f6-bef1-8e8002016b3b Co-authored-by: keesvandieren <863966+keesvandieren@users.noreply.github.com> --- .../pay/ios/apple/PurchaseManageriOSApple.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java index 9b5dd26..2adb53e 100644 --- a/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java +++ b/gdx-pay-iosrobovm-apple/src/main/java/com/badlogic/gdx/pay/ios/apple/PurchaseManageriOSApple.java @@ -18,10 +18,10 @@ import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.pay.*; +import com.badlogic.gdx.utils.Timer; import libcore.io.Base64; import org.robovm.apple.foundation.*; import org.robovm.apple.storekit.*; -import org.robovm.objc.block.VoidBlock1; import javax.annotation.Nullable; import java.util.ArrayList; @@ -53,7 +53,7 @@ public class PurchaseManageriOSApple implements PurchaseManager { private NSArray products; private final List restoredTransactions = new ArrayList(); - private NSTimer restoreTimer; + private Timer.Task restoreTimerTask; @Override public String storeName () { @@ -497,14 +497,14 @@ public void restoreCompletedTransactionsFinished (SKPaymentQueue queue) { // Defer delivery to allow any pending restored transactions to be processed. // 500ms is chosen to safely exceed the observed ~60ms gap between callbacks. cancelRestoreTimer(); - restoreTimer = NSTimer.createScheduled(0.5, false, new VoidBlock1() { + restoreTimerTask = Timer.schedule(new Timer.Task() { @Override - public void invoke(NSTimer timer) { - restoreTimer = null; + public void run() { + restoreTimerTask = null; observer.handleRestore(restoredTransactions.toArray(new Transaction[restoredTransactions.size()])); restoredTransactions.clear(); } - }); + }, 0.5f); } @Override @@ -524,9 +524,9 @@ public void restoreCompletedTransactionsFailed (SKPaymentQueue queue, NSError er } private void cancelRestoreTimer() { - if (restoreTimer != null) { - restoreTimer.invalidate(); - restoreTimer = null; + if (restoreTimerTask != null) { + restoreTimerTask.cancel(); + restoreTimerTask = null; } }