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"

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?

I thought that XCode was supposed to create this?

Yes. If I were you I'd look at the timestamp on that file and see if Xcode has just created an empty file, or whether it has done nothing at all and that file is left over from earlier. And I'd look in the build logs.

As you can tell, I'm out of my depth here...

I recommend that you try to reproduce this in a tiny test project. Create a new project from one of the built-in templates, add that Swift file to it, and then see if the TTT-Swift.h file renders as expected.

If it doesn’t, post a link to your project here and I’ll take a look.

If it does, you have a working example and you can compare it to your non-working main project.

Share and Enjoy

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

I already have a test project. I also created a test project App->objective-C. With both of these when I add the receipt validation Swift file to the projects XCode asks me if I want to create a bridging header. It creates the name-Bridging-header.h.

This is not the bridging head I want. I want the name-Swift.h header. The name-bridging-headers are empty.

Shouldn't they at least have this:

#ifndef name_Bridging_Header_h

#define name_Bridging_Header_h

#endif /* name_Bridging_Header_h */

So XCode doesn't create the bridging header. Is there something missing in the build settings or something?

Update: things have gone from bad to worse. I removed the Swift files from both projects, build them and when I add the Swift file it adds it to the projects but doesn't ask to create a bridging header.

Update 2: If I delete the Swift files, clean and rebuild my test project and add the Swift File, XCode will automatically create the two bridging headers. They will have the #ifndef, #define #endifs in them but no Objective-C prototype in the name-Swift.h header for the Swift Function to verify receipts.

I removed the Swift files from both projects, build them and when I add the Swift file it adds it to the projects but doesn't ask to create a bridging header

Right. That’s expected. Xcode only suggests adding a bridging header the first time you add code in the other language.

As to why things aren’t working for you, it’s hard to say. I described my process in some detail in my earlier post and it worked just fine for me.

If you upload a test project somewhere and post the URL here, I’d be happy to take a look. It’d be best you started with a new project, following the steps in my my earlier post. That’ll avoid any confusion created by your valiant attempts to get this working (-:

IMPORTANT If you have problems posting the URL, see tip 14 in Quinn’s Top Ten DevForums Tips.

Share and Enjoy

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

I got this to work and this is REALLY WEIRD:

If I #import the name-Swift.h header in a .m file the project will build and I can use the MyAppTransaction object in my .m files.

BUT XCode doesn't actually create the file in my project directories. Apparently it's cryptic, invisible, implied or something. It would have saved me a huge amount of trouble if the developer documentation described this or if someone mentioned it.

Also I never would have guessed that you would have to invoke the method using that syntax. Are a lot of the calls to Swift objects as weird as that? Is there some documentation about this?

Update: I spoke too soon. This will work in my test project but not on a major project for a Mac App on the App Store. Something in the build settings?

It would be helpful if I could read the name-Swift.h header to see how to invoke the Swift object methods. The project runs with this invisible header included but it isn't visible in my project hierarchy, Find can't find it, Spotlight can't find it, Open Quickly can't find it and XCode -> Find can't find it. This is extremely unhelpful.

% find ~/Library/Developer/Xcode -name '*-Swift.h' -print

Also I never would have guessed that you would have to invoke the method using that syntax.

What syntax, exactly?

BUT XCode doesn't actually create the file in my project directories.

Correct. That header is a build product and thus gets placed in the build products directory.

If you want to see its contents, command click on it. For example, in my test project I have this line:

#import "Test764537-Swift.h"

If I command click on the text inside the quotes, it takes me to the generated header:

… lots of stuff elided …

SWIFT_CLASS("_TtC10Test76453716MyAppTransaction")
@interface MyAppTransaction : NSObject
+ (void)checkReceiptWithCompletionHandler:(void (^ _Nonnull)(NSString * _Nonnull))completionHandler;

… lots more stuff elided …

I can then chose File > Show in Finder to find out where it’s really located. In my case that happened to be /Users/quinn/Library/Developer/Xcode/DerivedData/Test764537-foivrohmevxnvtewlpwruoiwbkdd/Index.noindex/Build/Intermediates.noindex/Test764537.build/Debug/Test764537.build/DerivedSources/Test764537-Swift.h.

Hence my comment about it being a build product.

Open Quickly can't find it

I wouldn’t expect any of the others to work — again, this is a build product — but I would’ve expected File > Open Quickly to work, because it usually mirrors the behaviour of command click. IMO you should file a bug about that. Please post your bug number, just for the record.

What syntax, exactly?

Yeah, likewise. I’m not sure what syntax you’re having problems with.

Share and Enjoy

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

endecotp: the syntax that surprised be was:

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

I'm not having a problem with it but I never saw it until it was posted here. Also I don't know how Swift types map to Objective-C types. Quinn mentioned that String is equivalent to NSString. This makes sense but it would be useful if all of this was documented somewhere.

I never saw it until it was posted here.

It's a normal objC class-method call, passing a block. Maybe it's the block that is new to you?

Anyway, is this working for you now?

but I never saw it until it was posted here.

It’s quite standard. You can see similar examples all over our SDK. For example, the +[PHPhotoLibrary requestAuthorizationForAccessLevel:handler:] method uses this syntax.

it would be useful if all of this was documented somewhere.

You’ll find a bunch of useful interoperability info under the Language Interoperability with Objective-C and C group in the Swift docs. For example, Working with Foundation Types has a table for these bridged types.

Share and Enjoy

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

I was working on this today and I encountered more strange problems:

Even though the chekReceipt() Swift function returns a String (-> String), the MyAppTransaction class method in the automatically generated name-Swift.h file is defined as (void). This won’t work because if the verificationResult is either .unverified or there is an error I need to post an alert. You can only post an alert from the main thread. Calling one from the completion handler will cause a crash. I have to return a result to the main thread to create an alert. I tried to edit the name-Swift.h header to change the return type of the checkReceiptWithCompletionHandler method to string but I can’t, it’s apparently read only.

Within the scope of the completion handler you can use the status string, for example NSLog(@"status: %@", status);, but you can’t return it to the calling function. I was hoping to copy it to the calling function with something like this:

NSString* verifyWithStoreKit(void) { NSString* myString;

[MyAppTransaction checkReceiptWithCompletionHandler: ^(NSString * _Nonnull status)
 {
    myString = [[NSString string] initWithString: status];
}];
//@autoreleasepool {
    // Setup code that might create autoreleased objects goes here.
//}
return myString;

}

But Xcode won’t build instructions like this or myString is nil. What can I do?

Even though the chekReceipt() Swift function returns a String (-> String), the MyAppTransaction class method in the automatically generated name-Swift.h file is defined as (void).

The swift function returns a string asynchronously. Since objC doesn't have async functions, it returns void and passes the string later to the completion handler.

I tried to edit the name-Swift.h header to change the return type

That's not going to work.

Within the scope of the completion handler you can use the status string ... but you can’t return it to the calling function.

Correct, and there is no way to avoid that. You need to restructure to work with this.

I need to post an alert. You can only post an alert from the main thread. Calling one from the completion handler will cause a crash.

In that case, in the completion handler you can dispatch to the main thread to post the alert. Pseudo-code:

    dispatch_async(dispatch_get_main_queue(), ^{
      present_alert(message);
    });

Thank you so much. Theoretically I could return status from the block but this won't work in this case. I could also do something like post a notification. Does the Swift function have to be async?

Does the Swift function have to be async?

Yes, because AppTransaction.shared is async.

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