Skip to content

Conversation

@dkimitsa
Copy link

recently I've added bindings for StoreKit2 swift based API. and as quick test have implemented PurchaseManager using it.
I have not any experience using gdx-pay before, thus implementation might be not completely as expected or something might be broken. was able to run sample app with it.
Hope will be usefeul for contributors of gdx-pay quick start.

Besides noted above following to me mentioned:

  • iOS version check to applied before using API: some are available ios15 but some requires ios17. in some cases it will require to use StoreKit1 api altogether with new one( like PurchaseIntent starting in iOS 16.4 or paymentQueue(_:shouldAddStorePayment:for: otherwise).
  • currently bindings are hosted at my account but primary repository will be under MobiVM umbrella.
  • not everything can be transparently migrated, things like purchase signature is a bit different now.

hope this helps, while it is not migrated to MobiVM please open an issue if found there https://github.com/dkimitsa/robovm-cocoatouch-swift.

uses `io.github.dkimitsa.robovm:robopods-storekit-swift` bindings for Swift based StoreKit2 API.
implementation should be considered as POC and might some things might be broken.At least gdx-pay sample app was working with this manager.
@keesvandieren
Copy link
Member

Thanks, really great!

One thing I would like to have: fill in the Free trial info only if the async function product.getSubscription().isEligibleForIntroOffer() returns true.

That is also how it works for Android, if the free trial is already used, Google Play won't return the trial.

For Apple, it should be verified manually with isEligibleForIntroOffer which now we can do with storekit2. However the conversion from Product to Information is currently being done in a synchronous method.

@dkimitsa
Copy link
Author

@keesvandieren hi, have added check for isEligibleForIntroOffer(). sadly has no setup to check if it works as expected.

d66cd94

@dkimitsa dkimitsa requested a review from keesvandieren April 25, 2025 12:11
@keesvandieren
Copy link
Member

Hi, I have tried out the purchase manager. I see prices, can fetch products / Information instances. Some callbacks seem to be missing:

  • com.badlogic.gdx.pay.ios.apple.PurchaseManageriOSApple2#purchase: on success, it should call observer.handlePurchaseError() on error, observer.handlePurchase() on success, observer.handlePurchaseCanceled() on cancellation
  • On restore cancellation (by ignoring the popup) handleRestoreError() is called by other implementations, I think PurchaseManageriOSApple2 should do too.

Thanks for your work anyways, If you plan to complete them I would be happy to test it again.

@dkimitsa
Copy link
Author

hi @keesvandieren , sorry for replying late. have implemented missing callbacks. also StoreKit2 api wrapper was updated with missing StoreKitError

@sebaber
Copy link

sebaber commented Jul 2, 2025

Hi, i came to do a hand with testing. In my app, im having an issue with One-Time charge purchasing. The first time i can do successfuly but at the second time, i get an error, this is the print of the error im having:

default	07:16:29.785495-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	07:16:29.785547-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	07:16:29.793116-0300	xxx	Evaluating dispatch of UIEvent: 0x145058700; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	07:16:29.793258-0300	xxx	Evaluating dispatch of UIEvent: 0x145058700; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	07:16:29.828495-0300	xxx	Found unfinished transaction for yyy.

Is like the transaction is not consumed. Maybe i have doing something wrong in my client iOS purchase process for that transaction, but at the moment, iOS purchasing are working good in my app

Updated:
I try a few times after the first one, and is like that the purchases start to get in queue (but my first purchase was successfuly, but despite that it get queue in the purchase ios client proccess)

This is the log that verify what im saying:

default	08:04:36.456435-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.457043-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.455707-0300	xxx	Sending UIEvent type: 0; subtype: 0; to windows: 1
default	08:04:36.456175-0300	xxx	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x105f2ca90>; contextId: 0x84412A77
default	08:04:36.456621-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.456063-0300	xxx	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x105f2ca90>; contextId: 0x84412A77
default	08:04:36.456232-0300	xxx	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x105f2ca90>; contextId: 0x84412A77
default	08:04:36.456484-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.455759-0300	xxx	Sending UIEvent type: 0; subtype: 0; to windows: 1
default	08:04:36.456388-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.456530-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.457230-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.456284-0300	xxx	Sending UIEvent type: 0; subtype: 0; to window: <UIWindow: 0x105f2ca90>; contextId: 0x84412A77
default	08:04:36.457089-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.456996-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.456574-0300	xxx	[info] [INFO] : Sending Purchase MyOffer of pchs: 0
default	08:04:36.457137-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.457182-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy ...
default	08:04:36.476119-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476183-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476398-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476505-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.476732-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.476241-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476349-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476675-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.476557-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.476298-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 0
default	08:04:36.476454-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.476624-0300	xxx	Evaluating dispatch of UIEvent: 0x1069c38e0; type: 0; subtype: 0; backing type: 11; shouldSend: 0; ignoreInteractionEvents: 0, systemGestureStateChange: 1
default	08:04:36.505445-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505738-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded
default	08:04:36.505397-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505300-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505235-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505492-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505547-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded
default	08:04:36.505784-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded
default	08:04:36.505595-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded
default	08:04:36.505348-0300	xxx	Found unfinished transaction for yyy.
default	08:04:36.505644-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded
default	08:04:36.505692-0300	xxx	[GdxPay/AppleIOS] Purchasing product yyy complete Purchase succeeded

@dkimitsa
Copy link
Author

@sebaber
hi, I've played with non-consumable products, it seems to be working for me on Simulator.

[GdxPay/AppleIOS] Installing purchase observer...
[GdxPay/AppleIOS] Requesting products...
2025-07-16 16:36:49.288 IOSLauncher[14326:251853] [info] IAP: Installed
[GdxPay/AppleIOS] Products successfully received!
[GdxPay/AppleIOS] Startup purchase transaction restore started!
[GdxPay/AppleIOS] Transaction has been restored: 0
[GdxPay/AppleIOS] All transactions have been restored!
[GdxPay/AppleIOS] Purchasing product nonconsumable.racecar ...
2025-07-16 16:36:59.582 IOSLauncher[14326:251811] [debug] IOSApplication: paused
2025-07-16 16:37:14.464 IOSLauncher[14326:251811] [debug] IOSApplication: resumed
2025-07-16 16:37:15.531 IOSLauncher[14326:251811] [debug] IOSApplication: paused
[GdxPay/AppleIOS] Purchasing product nonconsumable.racecar complete Purchase succeeded
2025-07-16 16:37:20.314 IOSLauncher[14326:251811] [debug] IOSApplication: resumed
[GdxPay/AppleIOS] Purchasing product nonconsumable.racecar ...
[GdxPay/AppleIOS] Purchasing product nonconsumable.racecar complete Purchase succeeded

where all these logs other than [GdxPay/AppleIOS] are coming from in your case ?

@sebaber
Copy link

sebaber commented Jul 29, 2025

Sorry for the late. I try to test again but in my project i have to update lo a higher version of gradle (8.14.3) and i have an issue of compatibility when i was trying to generate maven files on Narwhal IDLE. Is it possible to update this repo to a higher version of gradle?

This is the error:

* What went wrong:
Multiple build operations failed.
    Could not create task ':gdx-pay-android-amazon:generateDebugRFile'.
    Could not create task ':gdx-pay-android-googlebilling:generateDebugRFile'.
    Could not create task ':gdx-pay-android-huawei:generateDebugRFile'.
Could not create task ':gdx-pay-android-amazon:generateDebugRFile'.
Cannot use @TaskAction annotation on method IncrementalTask.taskAction$gradle_core() because interface org.gradle.api.tasks.incremental.IncrementalTaskInputs is not a valid parameter to an action method.

@sebaber
Copy link

sebaber commented Jul 29, 2025

Although, i think that these logs are from the class PurchaseManageriOSApple2 from purchase method

@Override
    public void purchase(final String identifier) {
        // Find the SKProduct for this identifier.
        String identifierForStore = config.getOffer(identifier).getIdentifierForStore(PurchaseManagerConfig.STORE_NAME_IOS_APPLE);
        Product product = getProductByStoreIdentifier(identifierForStore);
        if (product == null) {
            // Product with this identifier not found: load product info first and try to purchase again
            log(LOGTYPELOG, "Requesting product info for " + identifierForStore);

            if (productsRequestAndPurchase != null) productsRequestAndPurchase.cancel();
            productsRequestAndPurchase = getProducts(Collections.singleton(identifierForStore), new FetchProductAndPurchaseDelegate(), false);
        } else {
            // Create a SKPayment from the product and start purchase flow
            **log(LOGTYPELOG, "Purchasing product " + identifier + " ...");**
            product.purchase(new NSSet<Product.PurchaseOption>(), new VoidBlock2<Product.PurchaseResult, NSError>() {
                @Override
                public void invoke(Product.PurchaseResult purchaseResult, NSError nsError) {
                    if (purchaseResult != null) {
                        **log(LOGTYPELOG, "Purchasing product " + identifier + " complete " + purchaseResult);**

                        if (purchaseResult instanceof Product.PurchaseResult.success) {
                            Product.PurchaseResult.success success = (Product.PurchaseResult.success) purchaseResult;
                            // Product was successfully purchased.
                            final Transaction transaction = success.getTransaction().getUnsafePayloadValue();
                            // Parse transaction data.
                            final com.badlogic.gdx.pay.Transaction t = transaction(transaction);
                            if (t == null)
                                observer.handlePurchaseError(new GdxPayException("Failed to create GdxPay transaction"));
                            else
                                observer.handlePurchase(t);
                        } else if (purchaseResult == Product.PurchaseResult.userCancelled()) {
                            observer.handlePurchaseCanceled();
                        } else {
                            // should not happen
                            observer.handlePurchaseError(new GdxPayException("Unexpected purchase result " + purchaseResult));
                        }
                    } else {
                        String message = "Purchasing product " + identifier + " failed with error: " + nsError;
                        log(LOGTYPEERROR, message);
                        observer.handlePurchaseError(new GdxPayException(message));
                    }
                }
            });
        }
    }

For some reason for me, is like the purchase still remains in a queue after purchase, so next purchase send twice and start to increment in every next purchase

@keesvandieren
Copy link
Member

For me this is quite hard to test. We have (in our app) only one single purchase (subscription), not multiple purchases.

Anyone else with an app with consumables or multiple products who can tests this?

@keesvandieren
Copy link
Member

Hi, I remember that ios always returns all historic transactions, when restoring purchases. This is also the case with the old implementation.

So, for subscriptions for examples, if a subscription transaction is expired, it is still returned on iOS. On Android (Google Play implementation) only active transactions will be returned.

So having historic payment / transactions on iOS is normal behaviour. Should we implement in gdx-pay that only currently valid transactions are returned? I don't know if that can be done easily.

@keesvandieren
Copy link
Member

Hi @dkimitsa, is this already ready to be merged?

Is this related to https://dkimitsa.github.io/2025/08/29/swift-missing-objc-class-structures/, are changes requested there already available for gdx-pay?

@dkimitsa
Copy link
Author

dkimitsa commented Oct 8, 2025

hi @keesvandieren
there preparation to be done:

  • we need setup a repo at MobiVM and for swift based bindings
  • release them to mavenCentral (as snapshots are limited in time)

issues mentioned in post is related to case when binaries are compiled for ios12. Anyway there is no need to compile ios15 sources against ios12 target so we can ignore these.

will speak with tom-ski about arranging a repose and processes to release bindings.

@dkimitsa
Copy link
Author

@keesvandieren
have create discussion MobiVM/robovm#819

@dkimitsa
Copy link
Author

dkimitsa commented Nov 4, 2025

hi @keesvandieren
sorry for a late update, but it took a bit of time.
StoreKit bindings were moved to https://github.com/MobiVM/robovm-cocoatouch-swift and released to Sonatype:

It was split into Java bindings:

implementation("com.mobidevelop.robovm:robopods-swift-storekit2:18.2.0.1")

and kotlin wrappers (with coroutine flavour):

implementation("com.mobidevelop.robovm:robopods-swift-storekit2-kt:18.2.0.1")

I've update this PR is proper dependency. Personally I don't use StoreKit2 but there are active users so we monitor things to see if there is going to be any issue.

Meanwhile, as part of gdx-pay I would consider to give user instructions on how to exclude bindings from transitive dependencies if it is not used yet. As it will introduce extra 1Mb application size due binary being linked.

@keesvandieren
Copy link
Member

Thanks @dkimitsa. I will have a look it it in one - two weeks.

Few questions:

  • Does this restrict which Robovm versions can be used?
  • Which iOS versions are supported?
  • Should everything in https://developer.apple.com/storekit/ be supported?
  • When using StoreKit 2, then StoreKit 1 libraries are still on the classpath, as they are in robovm_cocoatouch. We cannot make them optional. Correct?
  • I think storekit2-kt cannot be used with gdx-pay, but can be used when using storekit 2 directly without gdx-pay. Is that correct?

@dkimitsa
Copy link
Author

dkimitsa commented Nov 5, 2025

@keesvandieren

Does this restrict which Robovm versions can be used?

it was built against 2.3.23 but dependency listed as "provided". it will not be propagated as transitive dependency.same time there is nothing specific to depend on and older versions expected to work.

Which iOS versions are supported?

StoreKitRvm (swift to objc wrapper, native framework) was compiled with "min ios version 15"

Should everything in https://developer.apple.com/storekit/ be supported?

Most of api from Storekit2.swift was wrapped as it was in ios18.2. If something is missing we can add it any time.

When using StoreKit 2, then StoreKit 1 libraries are still on the classpath, as they are in robovm_cocoatouch. We cannot
make them optional. Correct?

Cocoa touch is always present and it's huge. But if this api is not used the will not be included into binary (tree shaker can make it even better) .

I think storekit2-kt cannot be used with gdx-pay, but can be used when using storekit 2 directly without gdx-pay. Is that correct?

yes, mostly its for kotlin projects. it will provide experience similar to swift concurrency, without callbacks etc.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants