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
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"

not to do what you describe “Write a short Swift function that calls StoreKit, and call that from objC”. Maybe you know about some documentation describing how to do this?

Just write a swift function (you’ve already done that, or start again with hello_world) and look at the generated bridging header; that will show the syntax of the objC call. (With some obfuscation probably).

Things to note:

  1. There is the @objc annotation.
  2. async functions need the withCompletionHndler stuff mentioned earlier.
  3. I recall having to write a class with a single static method, rather than a function. I don’t recall the details.
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"

Quinn, can you remind me why this has to be a class method, rather than just a global function?

AppTransaction is available on macOS 13. If you are supporting users on earlier OSes you have to check for this and use the (deprecated?) receipt validation technique. I use this:

BOOL runningOnAtleast(int majorVersion, int minorVersion, int patchVersion)
{
	NSOperatingSystemVersion OSVersion; //struct with three variables declared as NSIntegers:
	OSVersion.majorVersion = majorVersion;
	OSVersion.minorVersion = minorVersion;
	OSVersion.patchVersion = patchVersion;
	return [[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion: OSVersion];
}

Quinn, your code won't compile :

let verificationResult = try await AppTransaction.shared

reports an error:

AppTransaction is only available in macOS 13 or newer.

I'm running on 15.0.1 so It looks like I can only use AppTransaction if I target 13 or later. If I do this it is likely that my program will crash when some users launch it. bummer...

Right, if you need to support older OS versions you need a runtime check to determine which method to use. Do this (in objC) using if @available.

Quinn, your procedure doesn't work:

  1. When I create the Swift file, XCode doesn't ask me if I want to create a bridging header. The project won't build without it. Also XCode asks me what I want to subclass. Is NSObject right?

  2. The project won't compile without projectname-Swift.h

Manually creating a projectname-Swift.h seems to work - it is filled in by XCode.

The project won't build without projectname-Bridging-Header.h. Manually creating it and adding it to the project seems to work - XCode fills it in but I get an error:

"Did you forget to declare this file as an output of a script phase or custom build rule which produces it?"

Huh? Isn't this a header file I can #import into my project?

Also the undocumented API @available doesn't look usable because it checks the environment at runtime. What I would need would be conditional compilation, right?

XCode didn't automatically add the bridging header to my target so doing this fixed that issue. but the project won't build because AppTransaction isn't available for my build target 10.11.

Also the undocumented API @available doesn't look usable because it checks the environment at runtime. What I would need would be conditional compilation, right?

No. You need to check at runtime. Your aim is a single executable that will run in both older and newer OS versions. It needs to check at runtime and use either the old method or the new method, as approprite.

I’m sure @available is documented somewhere, keep searching.

No, endecotp, I can check what it's running on at run-time but that's not the problem. You can't build the project using AppTransaction unless you target at least 13. And there are some conversations about @available on these forums but not in the developer documentation.

Also if you target something like 10.15 you will get a whole bunch of compiler warnings about the security functions in your verification code being deprecated.

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

Won't build - unknown object MyAppTransaction. Maybe some guidance about what to put in the bridging header would help?

No, endecotp, I can check what it's running on at run-time but that's not the problem. You can't build the project using AppTransaction unless you target at least 13.

Ah yes; sorry, I was forgetting the difference between the min-version and the max-version. I forget the exact terminology. You need to “target” ( if that’s the right term) a newer version that has the Swift AppTransaction support and set the oldest supported version to the older version without AppTransaction that you still want to support.

And there are some conversations about @available on these forums but not in the developer documentation.

LMGTFY: https://developer.apple.com/documentation/xcode/running-code-on-a-specific-version

Also if you target something like 10.15 you will get a whole bunch of compiler warnings about the security functions in your verification code being deprecated.

It is possible to silence specific warnings for specific files. Either do that, or just ignore them.

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

Won't build - unknown object MyAppTransaction. Maybe some guidance about what to put in the bridging header would help?

YOU don‘t put anything in the bridging header - it gets created for you. It ought to have a readable objC declaration of MyAppTransaction; if it doesn’t, something has gone wrong, likely related to how you added that to the project etc. Having only done this once, last year I think, I have no idea of the exact steps needed.

Quinn, your code won't compile

Yeah it does, because I compiled it before I posted it (-: It may not compile in your app, but such is the nature of code that you get from other folks.

If you need to support older systems, you’ll have to conditionalise the code to only run on those systems. If I change the deployment target on my test app to macOS 12 and compile, I get this error:

    let verificationResult = try await AppTransaction.shared
 // ^ 'AppTransaction' is only available in macOS 13.0 or newer
 // …/MyAppTransaction.swift:9:48: Add 'if #available' version check
 // …/MyAppTransaction.swift:7:16: Add @available attribute to enclosing class method
 // …/MyAppTransaction.swift:4:7: Add @available attribute to enclosing class

The three fix-its represent different ways to address this solution. In this case you want the first. That converts the code to this:

if #available(macOS 13.0, *) {
    let verificationResult = try await AppTransaction.shared
} else {
    // Fallback on earlier versions
}
switch verificationResult {
case .unverified(_, _):
    return "NG"
case .verified(_):
    return "OK"
}

and it’s only a short jump to code that does the right thing on old systems:

if #available(macOS 13.0, *) {
    let verificationResult = try await AppTransaction.shared
    switch verificationResult {
    case .unverified(_, _):
        return "NG"
    case .verified(_):
        return "OK"
    }
} else {
    return "OLD"
}
Xcode didn't automatically add the bridging header

Just to be clear, there are two headers in play here:

  • The bridging header, TTT-Bridging-Header.h where TTT is the target name, is for calling Objective-C from Swift. Xcode sets this up when you add code for the other language to your target. It’s configured via the Objective-C Bridging Header build setting. You are expected to edit this by hand.

    It’s not relevant to this discussion because Tlaloc is not calling Objective-C from Swift.

  • The… well… I’m not entirely sure what this is called, but it’s has a name of the form TTT-Swift.h. This is for calling Swift from Objective-C. This is generated for you by the compiler.

    This is what’s relevant to this discussion.

Quinn, can you remind me why this has to be a class method, rather than just a global function?

A global function won’t work in this case. It would have to use C calling conventions and there is no supported way to declare a Swift global function that uses C calling conventions [1].

Share and Enjoy

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

[1] There is @_cdecl but it’s not finalised; see this thread on Swift Forums.

Quinn, thank you so much for your help. As you can tell endecotp and I tried to get this done by reading the developer documentation but there is a lot missing. A more complete set of documentation would be great.

The name-Swift.h header doesn't have a declaration for the verification function. What's wrong?

The name-Swift.h header doesn't have a declaration for the verification function. What's wrong?

What did you declare in your Swift file? (Post the code.)

What do you see in the generated header?

The name-Swift.h header was filled in by XCode:

#ifndef Chac_Swift_h
#define Chac_Swift_h

#endif /* Chac_Swift_h */

The Swift object.swift file:

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"
        }
    }
}

This will build if my target is at least 10.13 but there's no prototype in the name-Swift.h file. I thought that XCode was supposed to create this?

Calling [MyAppTransaction checkReceipt] from a .m file will result in an error - Class method +checkReceipt not found. Theoretically something in the -Swift.h file like:

@interface MyAppTransaction : NSObject
- (NSString*)checkReceipt;
@end

Should allow XCode to recognize the Class method, but what?

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