AppTransaction: how to use in ObjC apps (now that we are forced to use it after the exit(173) deprecation)

Hello

We are developers of a long-running game series and now reports have started to come in that users who install any of our previous games from the Mac App Store on OS X Sequoia are shown a popup claiming "The exit(173) API is no longer available". It's actually a lie, the mechanism is still there, the receipt generation still works and the game still runs afterwards. But the popup is confusing to users therefore we need to update the code.

Apparently the replacement for the old receipt generation mechanism is AppTransaction which does not exist for Objective C. We have attempted to use it using the Swift/ObjC interoperability and failed so far. The problem is that we need to call async methods in AppTransaction and all our attempts to make this work have failed so far. It seems as the actor/@MainActor concept is not supported by Swift/ObjC interoperability and without those concepts we don't know how to pass results from the async context to the callers from ObjC.

The lack of usable information and code online regarding this topic is highly frustrating. Apple really needs to provide better support for developers if they want us to continue to support the Mac platform with high quality games and applications on the Mac App Store.

We would appreciate if anyone can cook up a working sample code how to use AppTransaction in ObjC. Thanks in advance!

Answered by DTS Engineer in 810009022
Written by Tlaloc in 809755022
All I need to do is execute one line of Swift:

Now that’s something I can help with. Here how I did this:

  1. I created a new project from an Objective-C template.

  2. I choose File > New > File from Template and then chose the macOS > Cocoa Class template, with Swift as the language.

  3. This asked whether I want to create a bridging header. I allowed that, but it’s not relevant to this discussion.

  4. I edited the file to look like this:

    import StoreKit
    
    @objc
    class MyAppTransaction: NSObject {
    
        @objc
        class func checkReceipt() async -> String {
            do {
                let verificationResult = try await AppTransaction.shared
                switch verificationResult {
                case .unverified(_, _):
                    return "NG"
                case .verified(_):
                    return "OK"
                }
            } catch {
                return "ERR"
            }
        }
    }
    

    I’ve chosen silly status values. You can tweak these values, or even the types, as you wish. However, the types must be Objective-C representable. In this case I’m using String, which bridges to NSString.

  5. On the Objective-C side, I added this:

    #import "Test764537-Swift.h"
    

    and then called the Swift code like this:

    [MyAppTransaction checkReceiptWithCompletionHandler:^(NSString * _Nonnull status) {
        NSLog(@"status: %@", status);
    }];
    

I’m working with Xcode 16.0, but I don’t think there’s anything Xcode 16 specific about this.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thank you so much for your help. I haven't decided what version to target, but this is easy. The fact that my project has a valid receipt that I got by calling exit(173) made it easier. In my opinion deprecating this is a bad idea that will make it harder for developers.

I am in the same boat and am now confused - I thought the solution, for ObjC, was to use SKReceiptRefreshRequest to refresh the receipt?!

My personal problem, though, is that I cannot test my code now. I seem to need a fresh system that has never seen the app before. I tried to create a VM with Fusion 11.5.3 as well as 13.5.2, but in all cases, I was only able to install macOS 10.14 or 11, but then upgrading to macOS 13 or 14 always results in kernel panics during install or shortly thereafter, with all settings still being defaults (other than increasing CPU cores and memory). Of course, this needs to be done on an Intel CPU Mac, because the Silicon Macs don't support the App Store in VMs. This is so fooked up.

I thought the solution, for ObjC, was to use SKReceiptRefreshRequest to refresh the receipt?

That still works but is deprecated. (IIRC).

To call the StoreKit2 codeI have a class called AppValidator written in swift:

@available(macOS 13.0, *)
public class appValidator: NSObject {
	var errorDescription : String = ""
	
	@objc func error() -> String {
		return errorDescription
	}
	
	@objc func validateApp() async -> Int {
		do {
			let verificationResult = try await AppTransaction.shared
			
			switch verificationResult {
				case .verified(let appTransaction):
					// StoreKit verified that the user purchased this app and
					// the properties in the AppTransaction instance.
					errorDescription = "success, original version: \(appTransaction.originalAppVersion)"
					return 1
					
				case .unverified(let appTransaction, let error):
					// The app transaction didn't pass StoreKit's verification.
					// Handle unverified app transaction information according
					// to your business model.
					
					errorDescription = error.localizedDescription
					return 0
					}
			}
		}
		catch {
			// Handle errors.
			switch (error) {
				case StoreKitError.unknown:
					errorDescription = "storekit unknown error \(error.localizedDescription)"
					return -1
				case StoreKitError.networkError(let url_error):
					errorDescription = url_error.localizedDescription
					return -2
				case StoreKitError.systemError(let error):
					errorDescription = error.localizedDescription
					return 0
				case StoreKitError.userCancelled:
					errorDescription = "user canceled, \(error.localizedDescription)"
					return -3
				default:
					errorDescription = "Storekit error: \(error.localizedDescription)"
					return -4
			}
		}
	}
}

To call this is Obj-C I do:

if (@available(macOS 13.0, *)) {
			appValidator* av = [appValidator new];
			[av validateAppWithCompletionHandler:^(NSInteger result) {
				// handle the result. Call av.error to get the error, if needed
			}];
		}
}

I'm not exposing any Obj-C code to swift so my bridging header is empty.

Mostly this works just fine but I do get some unknown errors coming back from StoreKit, and some where the transaction timed out. I just continue when I get those errors.

HTH.

I tried this and it didn't work, and I'd already followed other instruction on how to integrate Swift into an Objective C project. Thing is, I absolutely trust The Eskimo so I knew the problem was my end!

So many things happening in my main project so I set up another with the same identity such that I could test. But when I was doing so there was one setting in the guides I couldn't remember off the top of my head I just ignored it. Got everything else set up, and Boom, it was working. Told you I trust The Eskimo!

Went looking for that setting and changed it back on my original and Boom it's able to call the Swift correctly too. Trust The Eskimo!

That setting was "Define Module" and keep it set to No.

There are other parts that you need to set up in addition to this one part, and the other thing I'd say which may help you is using the notification centre to go from Swift back to Objective C. That you can get calls back when you have success in your StoreKit classes.

AppTransaction: how to use in ObjC apps (now that we are forced to use it after the exit(173) deprecation)
 
 
Q