iOS IAP Restore Callbacks and Storekit 2 Support?

With the current IAP plugin on iOS, if a Restore Purchase is called and the user fails to enter their credentials then there is no callback fired. I notice in the plugin code that it isn’t implemented, and the link to the Apple API shows lots of deprecated code.

The same behaviour on Buy IAP calls an error callback, so it is likely possible.

Is it still be possible to add a callback for restore errors?

I’ve read that Storekit 2 needs Swift and we can’t use this with Defold extensions?

1 Like

We do support Swift in extension dependencies but not in the dependencies themselves. If Storekit 2 requires Swift then we need to look into adding support for Swift.

Deprecated code will eventually become a problem but it is not necessarily a problem right now. Could you please create an issue in extension-iap for this?

1 Like

With the help of ai I’m trying to implement the restore callbacks on iOS. I kinda think I’m getting somewhere - the callbacks are triggered - but there is a crash when I attempt to pass the message to Defold (and I’m pretty sure there is missing code there).

The crash happens in iap_ios.mm on line 451 where I am likely not pushing the correct values.

I wouldn’t be bothered too much, but I fear rejection from Apple (and fellow humans too, but mostly Apple in this case)

extension-iap-master.zip (283.2 KB)

extension-iap-master-iosrestore.zip (286.3 KB)

Got a working version! Might be of interest to somebody else in future.

Very much written by ChatGPT 5-mini with basic testing by myself. Still using Storekit 1.

For the listener I’m using this…

    -- Handle synthetic restore completion/failure sent as transaction events
    if type(transaction) == "table" and transaction.state ~= nil then
        if transaction.state == iap.TRANS_STATE_RESTORE_FINISHED then
            -- restore sequence finished (no per-product data in this synthetic event)
            is_working = false
            print("DEFOLD IAP > Restore completed")
            -- optionally act on restored_products table here
            return
        elseif transaction.state == iap.TRANS_STATE_RESTORE_FAILED then
            -- restore failed, error table is provided as second arg (may be nil)
            is_working = false
            local err = error or { error = "Unknown error", reason = iap.REASON_UNSPECIFIED }
            print("DEFOLD IAP > Restore failed:", err.error, err.reason)
            return
        end
    end

	if error == nil then

		-- transaction.ident - product identifier ex: com.defold.my_iap_product
		-- transaction.state - see below
		-- transaction.date - date and time for transaction
		-- transaction.trans_ident - transaction identifier, only set when state is either 
			-- TRANS_STATE_RESTORED
			-- TRANS_STATE_UNVERIFIED
			-- TRANS_STATE_PURCHASED
		-- transaction.receipt - send to server to verify
		-- transaction.original_trans - Apple only when state is TRANS_STATE_RESTORED
		-- transaction.signature - Google only signature of purchase data
	
		if transaction.state == iap.TRANS_STATE_PURCHASING then
			--
		elseif transaction.state == iap.TRANS_STATE_PURCHASED then

			if owned_products[transaction.ident] == nil then
				owned_products[transaction.ident] = transaction
			end

			if iap.get_provider_id() == iap.PROVIDER_ID_APPLE then -- permanent Apple products must always be finished
				iap.finish(transaction)
			elseif iap.get_provider_id() == iap.PROVIDER_ID_GOOGLE then
				iap.acknowledge(transaction)
			end
			is_working = false

		elseif transaction.state == iap.TRANS_STATE_UNVERIFIED then
			-- https://defold.com/manuals/iap/#asynchronous-payments
			is_working = false

		elseif transaction.state == iap.TRANS_STATE_FAILED then
			is_working = false
			
		elseif transaction.state == iap.TRANS_STATE_RESTORED then
            restored_products[transaction.ident] = transaction
			is_working = false
		end
	else
		-- error.reason can be
		-- iap.REASON_UNSPECIFIED
		-- iap.REASON_USER_CANCELED

		if error.reason == iap.REASON_UNSPECIFIED then
			is_working = false
		elseif error.reason == iap.REASON_USER_CANCELED then
			is_working = false
		end
	end
end